package orchestrator

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/acp"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/jobs"
	"github.com/yan1h/agent-coding-workflow/internal/project"
)

type stepHandlerHarness struct {
	repo    *fakeRepo
	pm      *fakePM
	turns   *fakeTurnRunner
	sess    *fakeACPSessions
	lookup  *fakeLookup
	jobs    *fakeJobs
	audit   *fakeAudit
	handler *StepHandler
	run     *Run
}

func newStepHandlerHarness(t *testing.T, gate PhaseGate, startPhase project.Phase, maxAttempts int) *stepHandlerHarness {
	t.Helper()
	repo := newFakeRepo()
	wsID := uuid.New()
	akID := uuid.New()
	pm := &fakePM{
		proj: &project.Project{ID: uuid.New(), Slug: "demo"},
		req:  &project.Requirement{ID: uuid.New(), Number: 3, Title: "T", WorkspaceID: &wsID, Phase: startPhase, Status: project.StatusOpen},
	}
	run := &Run{
		ID: uuid.New(), ProjectID: pm.proj.ID, RequirementID: pm.req.ID,
		WorkspaceID: wsID, OwnerID: uuid.New(), Status: RunRunning,
		CurrentPhase: startPhase,
		Config:       RunConfig{MaxAttemptsPerPhase: maxAttempts, PhaseAgentKinds: allPhaseKinds(akID)},
	}
	repo.runs[run.ID] = run

	turns := &fakeTurnRunner{stopReason: "end_turn"}
	sess := &fakeACPSessions{}
	lookup := newFakeLookup()
	jq := &fakeJobs{}
	audit := &fakeAudit{}

	svc := NewService(ServiceDeps{
		Repo: repo, PM: pm, ACL: fakeACL{allow: true}, Jobs: jq,
		Defaults: allPhaseKinds(akID), Config: Config{MaxAttemptsPerPhase: maxAttempts}, Audit: audit,
	})

	h := NewStepHandler(StepHandlerDeps{
		Repo:             repo,
		Runner:           NewRunner(sess, turns, lookup, time.Minute),
		Gate:             gate,
		PM:               pm,
		Enqueue:          svc.(StepEnqueuer),
		DefaultAgentKind: allPhaseKinds(akID),
		Config:           Config{MaxAttemptsPerPhase: maxAttempts},
		Audit:            audit,
	})

	return &stepHandlerHarness{repo: repo, pm: pm, turns: turns, sess: sess, lookup: lookup, jobs: jq, audit: audit, handler: h, run: run}
}

func allPhaseKinds(akID uuid.UUID) map[project.Phase]uuid.UUID {
	m := map[project.Phase]uuid.UUID{}
	for _, p := range project.AllPhases {
		m[p] = akID
	}
	return m
}

func (h *stepHandlerHarness) seedStep(phase project.Phase) *Step {
	step := &Step{
		ID: uuid.New(), RunID: h.run.ID, Phase: phase, Attempt: 1,
		Status: StepPending, AgentKindID: h.run.Config.PhaseAgentKinds[phase], Prompt: "do it",
	}
	h.repo.steps[step.ID] = step
	return step
}

func jobFor(step *Step) jobs.Job {
	payload, _ := json.Marshal(stepPayload{RunID: step.RunID, StepID: step.ID})
	return jobs.Job{ID: uuid.New(), Type: JobTypeOrchestratorStep, Payload: payload}
}

func TestStepHandler_HappyAdvancesAndEnqueuesNext(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: true}}, project.PhasePlanning, 3)
	step := h.seedStep(project.PhasePlanning)

	if err := h.handler.Handle(context.Background(), jobFor(step)); err != nil {
		t.Fatalf("Handle: %v", err)
	}
	got, _ := h.repo.GetStep(context.Background(), step.ID)
	if got.Status != StepSucceeded {
		t.Errorf("step status = %s; want succeeded", got.Status)
	}
	if h.jobs.count() != 1 {
		t.Errorf("expected 1 next-step enqueued; got %d", h.jobs.count())
	}
	if len(h.pm.phaseWrites) != 1 || h.pm.phaseWrites[0] != project.PhasePrototyping {
		t.Errorf("expected phase advance to prototyping; got %v", h.pm.phaseWrites)
	}
	if !h.audit.has("orchestrator.step.advance") {
		t.Error("expected step.advance audit")
	}
}

func TestStepHandler_PhaseGateFailurePausesWithoutAdvancing(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: true}}, project.PhaseAuditing, 3)
	h.pm.phaseErr = errs.New(errs.CodePhaseGateFailed, "audit artifact not approved")
	step := h.seedStep(project.PhaseAuditing)

	if err := h.handler.Handle(context.Background(), jobFor(step)); err != nil {
		t.Fatalf("Handle should complete job and pause run on external phase gate; got %v", err)
	}
	got, _ := h.repo.GetStep(context.Background(), step.ID)
	if got.Status != StepAwaitingGate {
		t.Errorf("step status = %s; want awaiting_gate", got.Status)
	}
	if got.Attempt != 1 {
		t.Errorf("phase gate pause must not consume agent attempt; got %d", got.Attempt)
	}
	gotRun, _ := h.repo.GetRun(context.Background(), h.run.ID)
	if gotRun.Status != RunPaused {
		t.Errorf("run status = %s; want paused", gotRun.Status)
	}
	if gotRun.CurrentPhase != project.PhaseAuditing {
		t.Errorf("run phase = %s; want auditing", gotRun.CurrentPhase)
	}
	if h.pm.req.Phase != project.PhaseAuditing {
		t.Errorf("requirement phase = %s; want auditing", h.pm.req.Phase)
	}
	if h.jobs.count() != 0 {
		t.Errorf("next step must not be enqueued when project gate blocks; got %d", h.jobs.count())
	}
	if !h.audit.has("orchestrator.step.phase_blocked") {
		t.Error("expected phase_blocked audit")
	}
}

func TestStepHandler_PhaseWriteFailureRetriesWithoutAdvancing(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: true}}, project.PhasePlanning, 3)
	h.pm.phaseErr = errors.New("db temporarily unavailable")
	step := h.seedStep(project.PhasePlanning)

	err := h.handler.Handle(context.Background(), jobFor(step))
	if err == nil {
		t.Fatal("expected retryable error")
	}
	if jobs.IsPermanent(err) {
		t.Fatalf("expected retryable error; got permanent: %v", err)
	}
	got, _ := h.repo.GetStep(context.Background(), step.ID)
	if got.Status != StepAwaitingGate {
		t.Errorf("step status = %s; want awaiting_gate", got.Status)
	}
	if got.Attempt != 2 {
		t.Errorf("attempt = %d; want 2", got.Attempt)
	}
	gotRun, _ := h.repo.GetRun(context.Background(), h.run.ID)
	if gotRun.CurrentPhase != project.PhasePlanning {
		t.Errorf("run phase = %s; want planning", gotRun.CurrentPhase)
	}
	if h.jobs.count() != 0 {
		t.Errorf("next step must not be enqueued after failed phase write; got %d", h.jobs.count())
	}
}

func TestStepHandler_AwaitingGateResumeDoesNotRerunAgent(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: true}}, project.PhasePlanning, 3)
	step := h.seedStep(project.PhasePlanning)
	gate := GateResult{Pass: true, Reason: "ready"}
	step.Status = StepAwaitingGate
	step.GateResult = &gate
	h.repo.steps[step.ID] = step

	if err := h.handler.Handle(context.Background(), jobFor(step)); err != nil {
		t.Fatalf("Handle: %v", err)
	}
	if h.turns.calls != 0 {
		t.Errorf("awaiting gate resume should not rerun agent; calls=%d", h.turns.calls)
	}
	if len(h.sess.created) != 0 {
		t.Errorf("awaiting gate resume should not create session; got %d", len(h.sess.created))
	}
	if h.jobs.count() != 1 {
		t.Errorf("expected next step enqueued after phase write succeeds; got %d", h.jobs.count())
	}
}

func TestStepHandler_GateRetryReturnsRetryableError(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: false, Retry: true, Reason: "no artifact"}}, project.PhasePlanning, 3)
	step := h.seedStep(project.PhasePlanning)

	err := h.handler.Handle(context.Background(), jobFor(step))
	if err == nil {
		t.Fatal("expected retryable error")
	}
	if jobs.IsPermanent(err) {
		t.Errorf("expected non-permanent error; got permanent: %v", err)
	}
	got, _ := h.repo.GetStep(context.Background(), step.ID)
	if got.Attempt != 2 {
		t.Errorf("attempt = %d; want 2", got.Attempt)
	}
	if h.jobs.count() != 0 {
		t.Errorf("no next step should be enqueued on retry; got %d", h.jobs.count())
	}
}

func TestStepHandler_AttemptsExhaustedDeadLetters(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: false, Retry: true}}, project.PhasePlanning, 1)
	step := h.seedStep(project.PhasePlanning) // attempt already 1 == max

	err := h.handler.Handle(context.Background(), jobFor(step))
	if !jobs.IsPermanent(err) {
		t.Fatalf("expected permanent error; got %v", err)
	}
	got, _ := h.repo.GetStep(context.Background(), step.ID)
	if got.Status != StepDead {
		t.Errorf("step status = %s; want dead", got.Status)
	}
	gotRun, _ := h.repo.GetRun(context.Background(), h.run.ID)
	if gotRun.Status != RunFailed {
		t.Errorf("run status = %s; want failed", gotRun.Status)
	}
	if !h.audit.has("orchestrator.step.dead_letter") {
		t.Error("expected dead_letter audit")
	}
}

func TestStepHandler_GateNoRetryImmediateDeadLetter(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: false, Retry: false, Reason: "refusal"}}, project.PhasePlanning, 5)
	step := h.seedStep(project.PhasePlanning)

	err := h.handler.Handle(context.Background(), jobFor(step))
	if !jobs.IsPermanent(err) {
		t.Fatalf("expected permanent error on no-retry gate; got %v", err)
	}
	got, _ := h.repo.GetStep(context.Background(), step.ID)
	if got.Status != StepDead {
		t.Errorf("step status = %s; want dead", got.Status)
	}
}

func TestStepHandler_PausedRunNoOp(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: true}}, project.PhasePlanning, 3)
	h.run.Status = RunPaused
	h.repo.runs[h.run.ID].Status = RunPaused
	step := h.seedStep(project.PhasePlanning)

	if err := h.handler.Handle(context.Background(), jobFor(step)); err != nil {
		t.Fatalf("Handle should no-op complete; got %v", err)
	}
	if h.turns.calls != 0 {
		t.Errorf("paused run should not drive a turn; calls=%d", h.turns.calls)
	}
	if len(h.sess.created) != 0 {
		t.Errorf("paused run should not spawn a session; got %d", len(h.sess.created))
	}
}

func TestStepHandler_DoneMarksRunSucceeded(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: true}}, project.PhaseDone, 3)
	step := h.seedStep(project.PhaseDone)

	if err := h.handler.Handle(context.Background(), jobFor(step)); err != nil {
		t.Fatalf("Handle: %v", err)
	}
	gotRun, _ := h.repo.GetRun(context.Background(), h.run.ID)
	if gotRun.Status != RunSucceeded {
		t.Errorf("run status = %s; want succeeded", gotRun.Status)
	}
	if h.jobs.count() != 0 {
		t.Errorf("done should enqueue nothing; got %d", h.jobs.count())
	}
	if !h.audit.has("orchestrator.run.succeeded") {
		t.Error("expected run.succeeded audit")
	}
}

func TestStepHandler_ResumesCrashedSession(t *testing.T) {
	h := newStepHandlerHarness(t, fakeGate{result: GateResult{Pass: true}}, project.PhasePlanning, 3)
	step := h.seedStep(project.PhasePlanning)
	// 该 step 已有崩溃 session：driveTurn 应走 resume，而非 Create。
	crashedID := uuid.New()
	h.lookup.crashed[step.ID] = &acp.Session{ID: crashedID, Status: acp.SessionCrashed}

	if err := h.handler.Handle(context.Background(), jobFor(step)); err != nil {
		t.Fatalf("Handle: %v", err)
	}
	if len(h.sess.created) != 0 {
		t.Errorf("should resume not create; created=%d", len(h.sess.created))
	}
	got, _ := h.repo.GetStep(context.Background(), step.ID)
	if got.ACPSessionID == nil || *got.ACPSessionID != crashedID {
		t.Errorf("step should reference resumed session %s; got %v", crashedID, got.ACPSessionID)
	}
}
