package orchestrator

import (
	"context"
	"errors"
	"testing"

	"github.com/google/uuid"

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

// codeOf 从 error 链中提取 errs.Code（测试断言用）。
func codeOf(err error) errs.Code {
	var ae *errs.AppError
	if errors.As(err, &ae) {
		return ae.Code
	}
	return ""
}

func newServiceHarness(maxDepth, fanOut int) (*service, *fakeRepo, *fakePM, *fakeJobs, uuid.UUID) {
	repo := newFakeRepo()
	wsID := uuid.New()
	akID := uuid.New()
	ownerID := uuid.New()
	pm := &fakePM{
		proj: &project.Project{ID: uuid.New(), Slug: "demo", OwnerID: ownerID},
		req:  &project.Requirement{ID: uuid.New(), Number: 5, Title: "T", WorkspaceID: &wsID, Phase: project.PhasePlanning, Status: project.StatusOpen, OwnerID: ownerID},
	}
	jq := &fakeJobs{}
	svc := NewService(ServiceDeps{
		Repo: repo, PM: pm, ACL: fakeACL{allow: true}, Jobs: jq,
		Sessions: &fakeSessTerm{},
		Defaults: allPhaseKinds(akID),
		Config:   Config{MaxAttemptsPerPhase: 3, MaxDepth: maxDepth, FanOutLimit: fanOut},
		Audit:    &fakeAudit{},
	}).(*service)
	return svc, repo, pm, jq, akID
}

func TestStartRun_CreatesRunStepAndEnqueues(t *testing.T) {
	svc, repo, _, jq, _ := newServiceHarness(3, 3)
	run, err := svc.StartRun(context.Background(), Caller{UserID: uuid.New()}, StartRunInput{
		ProjectSlug: "demo", RequirementNumber: 5,
	})
	if err != nil {
		t.Fatalf("StartRun: %v", err)
	}
	if run.Status != RunPending {
		t.Errorf("run status = %s; want pending", run.Status)
	}
	steps, _ := repo.ListStepsByRun(context.Background(), run.ID)
	if len(steps) != 1 {
		t.Fatalf("expected 1 step; got %d", len(steps))
	}
	if steps[0].Phase != project.PhasePlanning {
		t.Errorf("first step phase = %s; want planning", steps[0].Phase)
	}
	if jq.count() != 1 {
		t.Errorf("expected 1 job enqueued; got %d", jq.count())
	}
}

func TestStartRun_UsesProjectOwnerAsExecutionOwner(t *testing.T) {
	svc, _, pm, _, _ := newServiceHarness(3, 3)
	starter := uuid.New()

	run, err := svc.StartRun(context.Background(), Caller{UserID: starter, IsAdmin: true}, StartRunInput{
		ProjectSlug: "demo", RequirementNumber: 5,
	})
	if err != nil {
		t.Fatalf("StartRun: %v", err)
	}
	if run.OwnerID != pm.proj.OwnerID {
		t.Errorf("run owner = %s; want project owner %s", run.OwnerID, pm.proj.OwnerID)
	}
	if run.OwnerID == starter {
		t.Error("run owner must be the autonomous execution identity, not the starter")
	}
}

func TestStartRun_ForbiddenWithoutWriteAccess(t *testing.T) {
	repo := newFakeRepo()
	wsID := uuid.New()
	pm := &fakePM{
		proj: &project.Project{ID: uuid.New(), Slug: "demo"},
		req:  &project.Requirement{ID: uuid.New(), Number: 5, WorkspaceID: &wsID, Status: project.StatusOpen},
	}
	svc := NewService(ServiceDeps{
		Repo: repo, PM: pm, ACL: fakeACL{allow: false}, Jobs: &fakeJobs{},
		Defaults: allPhaseKinds(uuid.New()), Config: Config{MaxAttemptsPerPhase: 3}, Audit: &fakeAudit{},
	})
	_, err := svc.StartRun(context.Background(), Caller{UserID: uuid.New()}, StartRunInput{ProjectSlug: "demo", RequirementNumber: 5})
	if err == nil || codeOf(err) != errs.CodeForbidden {
		t.Fatalf("expected forbidden; got %v", err)
	}
}

func TestGetRun_AllowsProjectReader(t *testing.T) {
	svc, repo, pm, _, _ := newServiceHarness(3, 3)
	reader := uuid.New()
	run := &Run{
		ID: uuid.New(), ProjectID: pm.proj.ID, RequirementID: pm.req.ID, WorkspaceID: *pm.req.WorkspaceID,
		OwnerID: pm.req.OwnerID, Status: RunRunning, CurrentPhase: project.PhasePlanning,
	}
	repo.runs[run.ID] = run
	svc.acl = fakeACL{read: true}

	got, err := svc.GetRun(context.Background(), Caller{UserID: reader}, run.ID)
	if err != nil {
		t.Fatalf("GetRun: %v", err)
	}
	if got.ID != run.ID {
		t.Errorf("run id = %s; want %s", got.ID, run.ID)
	}
}

func TestStartRun_RequiresWorkspace(t *testing.T) {
	repo := newFakeRepo()
	pm := &fakePM{
		proj: &project.Project{ID: uuid.New(), Slug: "demo"},
		req:  &project.Requirement{ID: uuid.New(), Number: 5, WorkspaceID: nil, Status: project.StatusOpen},
	}
	svc := NewService(ServiceDeps{
		Repo: repo, PM: pm, ACL: fakeACL{allow: true}, Jobs: &fakeJobs{},
		Defaults: allPhaseKinds(uuid.New()), Config: Config{MaxAttemptsPerPhase: 3}, Audit: &fakeAudit{},
	})
	_, err := svc.StartRun(context.Background(), Caller{UserID: uuid.New()}, StartRunInput{ProjectSlug: "demo", RequirementNumber: 5})
	if err == nil {
		t.Fatal("expected error for requirement without workspace")
	}
}

// ===== RequestSubtask =====

func seedParentStep(svc *service, repo *fakeRepo, akID uuid.UUID, depth int) (*Run, *Step, uuid.UUID) {
	wsID := uuid.New()
	run := &Run{
		ID: uuid.New(), ProjectID: uuid.New(), RequirementID: uuid.New(), WorkspaceID: wsID,
		OwnerID: uuid.New(), Status: RunRunning, CurrentPhase: project.PhaseImplementing,
		Config: RunConfig{PhaseAgentKinds: allPhaseKinds(akID)},
	}
	repo.runs[run.ID] = run
	sessID := uuid.New()
	parent := &Step{
		ID: uuid.New(), RunID: run.ID, Phase: project.PhaseImplementing, Attempt: 1, Depth: depth,
		Status: StepRunning, AgentKindID: akID, ACPSessionID: &sessID, Prompt: "p",
	}
	repo.steps[parent.ID] = parent
	return run, parent, sessID
}

func TestRequestSubtask_HappyInsertsChildAndEnqueues(t *testing.T) {
	svc, repo, _, jq, akID := newServiceHarness(3, 3)
	_, _, parentSession := seedParentStep(svc, repo, akID, 0)

	child, err := svc.RequestSubtask(context.Background(), parentSession, SubtaskInput{
		Phase: project.PhaseImplementing, Prompt: "do subtask",
	})
	if err != nil {
		t.Fatalf("RequestSubtask: %v", err)
	}
	if child.Depth != 1 || child.ParentStepID == nil {
		t.Errorf("child depth/parent wrong: depth=%d parent=%v", child.Depth, child.ParentStepID)
	}
	if jq.count() != 1 {
		t.Errorf("expected child step enqueued; got %d", jq.count())
	}
}

func TestRequestSubtask_DepthLimit(t *testing.T) {
	svc, repo, _, _, akID := newServiceHarness(2, 5)
	// parent depth 2 == MaxDepth; child would be depth 3 → rejected.
	_, _, parentSession := seedParentStep(svc, repo, akID, 2)

	_, err := svc.RequestSubtask(context.Background(), parentSession, SubtaskInput{Prompt: "x"})
	if err == nil || codeOf(err) != errs.CodeForbidden {
		t.Fatalf("expected forbidden depth; got %v", err)
	}
}

func TestRequestSubtask_FanOutLimit(t *testing.T) {
	svc, repo, _, _, akID := newServiceHarness(5, 1)
	run, parent, parentSession := seedParentStep(svc, repo, akID, 0)
	// 已有一个活跃子 step，FanOutLimit=1 → 再请求被拒。
	pid := parent.ID
	repo.steps[uuid.New()] = &Step{
		ID: uuid.New(), RunID: run.ID, Phase: project.PhaseImplementing, ParentStepID: &pid,
		Depth: 1, Status: StepRunning, AgentKindID: akID,
	}
	_, err := svc.RequestSubtask(context.Background(), parentSession, SubtaskInput{Prompt: "x"})
	if err == nil || codeOf(err) != errs.CodeForbidden {
		t.Fatalf("expected forbidden fan-out; got %v", err)
	}
}

func TestRequestSubtask_RejectsUnknownSession(t *testing.T) {
	svc, _, _, _, _ := newServiceHarness(3, 3)
	_, err := svc.RequestSubtask(context.Background(), uuid.New(), SubtaskInput{Prompt: "x"})
	if err == nil || codeOf(err) != errs.CodeForbidden {
		t.Fatalf("expected forbidden for unknown session; got %v", err)
	}
}

// ===== Pause/Cancel/Resume =====

func TestPauseTerminatesActiveSessions(t *testing.T) {
	repo := newFakeRepo()
	term := &fakeSessTerm{}
	akID := uuid.New()
	wsID := uuid.New()
	owner := uuid.New()
	run := &Run{ID: uuid.New(), ProjectID: uuid.New(), RequirementID: uuid.New(), WorkspaceID: wsID, OwnerID: owner, Status: RunRunning, CurrentPhase: project.PhasePlanning}
	repo.runs[run.ID] = run
	sessID := uuid.New()
	repo.steps[uuid.New()] = &Step{ID: uuid.New(), RunID: run.ID, Phase: project.PhasePlanning, Status: StepRunning, AgentKindID: akID, ACPSessionID: &sessID}

	svc := NewService(ServiceDeps{Repo: repo, PM: &fakePM{}, ACL: fakeACL{allow: true}, Jobs: &fakeJobs{}, Sessions: term, Defaults: allPhaseKinds(akID), Config: Config{}, Audit: &fakeAudit{}})
	if err := svc.Pause(context.Background(), Caller{UserID: owner}, run.ID); err != nil {
		t.Fatalf("Pause: %v", err)
	}
	gotRun, _ := repo.GetRun(context.Background(), run.ID)
	if gotRun.Status != RunPaused {
		t.Errorf("run status = %s; want paused", gotRun.Status)
	}
	if len(term.terminated) != 1 || term.terminated[0] != sessID {
		t.Errorf("expected session %s terminated; got %v", sessID, term.terminated)
	}
}

// ===== ResetStuckStepsOnRestart =====

func TestResetStuckStepsOnRestart(t *testing.T) {
	repo := newFakeRepo()
	akID := uuid.New()
	activeRun := &Run{ID: uuid.New(), Status: RunRunning}
	doneRun := &Run{ID: uuid.New(), Status: RunSucceeded}
	repo.runs[activeRun.ID] = activeRun
	repo.runs[doneRun.ID] = doneRun

	stuckActive := &Step{ID: uuid.New(), RunID: activeRun.ID, Status: StepRunning, AgentKindID: akID}
	awaiting := &Step{ID: uuid.New(), RunID: activeRun.ID, Status: StepAwaitingGate, AgentKindID: akID}
	doneStep := &Step{ID: uuid.New(), RunID: doneRun.ID, Status: StepRunning, AgentKindID: akID}
	repo.steps[stuckActive.ID] = stuckActive
	repo.steps[awaiting.ID] = awaiting
	repo.steps[doneStep.ID] = doneStep

	reset, err := repo.ResetStuckStepsOnRestart(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if len(reset) != 2 {
		t.Fatalf("expected 2 reset steps; got %d", len(reset))
	}
	if s, _ := repo.GetStep(context.Background(), doneStep.ID); s.Status != StepRunning {
		t.Errorf("step under succeeded run must be untouched; got %s", s.Status)
	}
	if s, _ := repo.GetStep(context.Background(), stuckActive.ID); s.Status != StepPending {
		t.Errorf("stuck step under active run should be pending; got %s", s.Status)
	}
}
