package runners_test

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/jobs/runners"
)

type fakeReaperRepo struct {
	sessions []runners.ReaperSession
	listErr  error

	mu      sync.Mutex
	marked  map[uuid.UUID]string
	markErr error
}

func (f *fakeReaperRepo) ListSessionsForReaper(_ context.Context, _, _ time.Time) ([]runners.ReaperSession, error) {
	if f.listErr != nil {
		return nil, f.listErr
	}
	return f.sessions, nil
}

func (f *fakeReaperRepo) MarkSessionTerminatedReason(_ context.Context, id uuid.UUID, reason string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.marked == nil {
		f.marked = map[uuid.UUID]string{}
	}
	f.marked[id] = reason
	return f.markErr
}

type fakeKiller struct {
	mu     sync.Mutex
	killed []uuid.UUID
}

func (k *fakeKiller) Kill(_ context.Context, sid uuid.UUID, _ time.Duration) {
	k.mu.Lock()
	defer k.mu.Unlock()
	k.killed = append(k.killed, sid)
}

func TestAcpSessionReaper_KillsWallClockAndIdle(t *testing.T) {
	now := time.Now()
	wallSession := runners.ReaperSession{
		ID: uuid.New(), UserID: uuid.New(),
		StartedAt:      now.Add(-5 * time.Hour),
		LastActivityAt: now.Add(-1 * time.Minute), // active, but wall-clock exceeded
	}
	idleSession := runners.ReaperSession{
		ID: uuid.New(), UserID: uuid.New(),
		StartedAt:      now.Add(-10 * time.Minute),
		LastActivityAt: now.Add(-1 * time.Hour), // idle
	}
	repo := &fakeReaperRepo{sessions: []runners.ReaperSession{wallSession, idleSession}}
	killer := &fakeKiller{}
	r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
		WallClockTimeout: 4 * time.Hour,
		IdleTimeout:      30 * time.Minute,
	}, discardLogger())

	r.Run(context.Background())

	assert.ElementsMatch(t, []uuid.UUID{wallSession.ID, idleSession.ID}, killer.killed)
	assert.Equal(t, "reaper_wall_clock", repo.marked[wallSession.ID])
	assert.Equal(t, "reaper_idle", repo.marked[idleSession.ID])
}

func TestAcpSessionReaper_PerSessionWallClock(t *testing.T) {
	now := time.Now()
	cap30s := int32(30)
	s := runners.ReaperSession{
		ID: uuid.New(), UserID: uuid.New(),
		StartedAt:           now.Add(-1 * time.Minute),
		LastActivityAt:      now,
		MaxWallClockSeconds: &cap30s, // exceeded per-session cap, no global cap set
	}
	repo := &fakeReaperRepo{sessions: []runners.ReaperSession{s}}
	killer := &fakeKiller{}
	r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
		IdleTimeout: time.Hour, // generous idle so only wall-clock triggers
	}, discardLogger())

	r.Run(context.Background())

	require.Len(t, killer.killed, 1)
	assert.Equal(t, s.ID, killer.killed[0])
	assert.Equal(t, "reaper_wall_clock", repo.marked[s.ID])
}

func TestAcpSessionReaper_SkipsWithinThresholds(t *testing.T) {
	now := time.Now()
	s := runners.ReaperSession{
		ID: uuid.New(), UserID: uuid.New(),
		StartedAt:      now.Add(-5 * time.Minute),
		LastActivityAt: now.Add(-1 * time.Minute),
	}
	repo := &fakeReaperRepo{sessions: []runners.ReaperSession{s}}
	killer := &fakeKiller{}
	r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
		WallClockTimeout: 4 * time.Hour,
		IdleTimeout:      30 * time.Minute,
	}, discardLogger())

	r.Run(context.Background())

	assert.Empty(t, killer.killed)
	assert.Empty(t, repo.marked)
}

func TestAcpSessionReaper_MarkErrorStillKills(t *testing.T) {
	now := time.Now()
	s := runners.ReaperSession{
		ID: uuid.New(), UserID: uuid.New(),
		StartedAt:      now.Add(-5 * time.Hour),
		LastActivityAt: now,
	}
	repo := &fakeReaperRepo{sessions: []runners.ReaperSession{s}, markErr: errors.New("db down")}
	killer := &fakeKiller{}
	r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
		WallClockTimeout: 4 * time.Hour,
		IdleTimeout:      30 * time.Minute,
	}, discardLogger())

	r.Run(context.Background())

	require.Len(t, killer.killed, 1)
}

func TestAcpSessionReaper_ListErrorIsNotFatal(t *testing.T) {
	repo := &fakeReaperRepo{listErr: errors.New("boom")}
	killer := &fakeKiller{}
	r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
		IdleTimeout: 30 * time.Minute,
	}, discardLogger())

	r.Run(context.Background())
	assert.Empty(t, killer.killed)
}
