package roadmapsyncpipeline

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/nomadcode/nomadcode-core/internal/db"
	"github.com/nomadcode/nomadcode-core/internal/roadmapsync"
	"github.com/nomadcode/nomadcode-core/internal/storage"
	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

const (
	testMilestonePath = "agent-roadmap/phase/p1/milestones/m1.md"
	testRevision      = "rev-abc"
)

// fakeProvider records the order of provider facet calls and can fail a chosen
// step so a stop-before-later-step test is possible.
type fakeProvider struct {
	calls        []string
	refCalls     []workitem.Ref
	failOn       string
	failErr      error
	createdInput workitem.CreateInput
}

func (f *fakeProvider) AppendComment(_ context.Context, input workitem.CommentInput) error {
	f.calls = append(f.calls, "comment")
	f.refCalls = append(f.refCalls, input.Ref)
	if f.failOn == "comment" {
		return f.failErr
	}
	return nil
}

func (f *fakeProvider) ProjectBody(_ context.Context, input workitem.BodyProjection) error {
	f.calls = append(f.calls, "body")
	f.refCalls = append(f.refCalls, input.Ref)
	if f.failOn == "body" {
		return f.failErr
	}
	return nil
}

func (f *fakeProvider) ProjectStatus(_ context.Context, input workitem.StatusProjection) error {
	f.calls = append(f.calls, "status")
	f.refCalls = append(f.refCalls, input.Ref)
	if f.failOn == "status" {
		return f.failErr
	}
	return nil
}

func (f *fakeProvider) CreateWorkItem(_ context.Context, input workitem.CreateInput) (workitem.CreateResult, error) {
	f.calls = append(f.calls, "create")
	f.createdInput = input
	if f.failOn == "create" {
		return workitem.CreateResult{}, f.failErr
	}
	return workitem.CreateResult{
		Ref: workitem.Ref{
			Provider: input.Provider,
			Tenant:   input.Tenant,
			Project:  input.Project,
			ID:       "CREATED-123",
		},
	}, nil
}

// fakeStore is an in-memory Store. identity is the single persisted row (id 0
// means "not found"); marked records the steps MarkStepCompleted was called
// with, in order.
type fakeStore struct {
	identity  db.RoadmapSyncIdentity
	found     bool
	completed map[storage.RoadmapSyncStep]bool
	marked    []storage.RoadmapSyncStep
	upserted  bool
	markErr   error
	upsertErr error
}

func newFakeStore() *fakeStore {
	return &fakeStore{completed: map[storage.RoadmapSyncStep]bool{}}
}

func (f *fakeStore) GetRoadmapSyncIdentityByMilestoneID(_ context.Context, milestoneID string) (db.RoadmapSyncIdentity, error) {
	if !f.found || f.identity.RoadmapItemID != milestoneID {
		return db.RoadmapSyncIdentity{}, storage.ErrRoadmapSyncIdentityNotFound
	}
	return f.identity, nil
}

func (f *fakeStore) GetRoadmapSyncIdentityByWorkItem(_ context.Context, _ db.GetRoadmapSyncIdentityByWorkItemParams) (db.RoadmapSyncIdentity, error) {
	if !f.found {
		return db.RoadmapSyncIdentity{}, storage.ErrRoadmapSyncIdentityNotFound
	}
	return f.identity, nil
}

func (f *fakeStore) UpsertRoadmapSyncIdentity(_ context.Context, args db.UpsertRoadmapSyncIdentityByMilestoneIDParams) (db.RoadmapSyncIdentity, error) {
	if f.upsertErr != nil {
		return db.RoadmapSyncIdentity{}, f.upsertErr
	}
	f.upserted = true
	f.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                args.Shape,
		RoadmapMilestonePath: args.RoadmapMilestonePath,
		RoadmapItemID:        args.RoadmapItemID,
		Provider:             args.Provider,
		Tenant:               args.Tenant,
		Project:              args.Project,
		WorkItemID:           args.WorkItemID,
		ProviderRevision:     args.ProviderRevision,
		RoadmapRevision:      args.RoadmapRevision,
	}
	f.found = true
	return f.identity, nil
}

func (f *fakeStore) CompletedSteps(_ context.Context, _ int64) (map[storage.RoadmapSyncStep]bool, error) {
	out := make(map[storage.RoadmapSyncStep]bool, len(f.completed))
	for k, v := range f.completed {
		out[k] = v
	}
	return out, nil
}

func (f *fakeStore) MarkStepCompleted(_ context.Context, _ int64, step storage.RoadmapSyncStep) (db.RoadmapSyncStep, error) {
	if f.markErr != nil {
		return db.RoadmapSyncStep{}, f.markErr
	}
	f.marked = append(f.marked, step)
	f.completed[step] = true
	return db.RoadmapSyncStep{Step: string(step)}, nil
}

func readyInput() SyncCreationInput {
	identity := roadmapsync.Identity{
		Shape:                roadmapsync.ShapeMilestone,
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-1",
	}
	return SyncCreationInput{
		Scan: roadmapsync.ScanResult{
			Revision:     roadmapsync.Revision{Branch: "develop", Revision: testRevision},
			ChangedFiles: []string{testMilestonePath},
			ScannedMilestones: []roadmapsync.ScannedMilestone{
				{Path: testMilestonePath, Identity: identity},
			},
		},
		Expected:          identity,
		Ref:               workitem.Ref{Provider: "plane", Tenant: "general", Project: "nomad", ID: "NOMAD-1"},
		OriginalBody:      "<p>user request</p>",
		TodoStateID:       "todo-state",
		MilestoneMarkdown: "# Milestone\n\nbody",
		ProviderRevision:  testRevision,
		RoadmapRevision:   testRevision,
		ExternalSource:    "nomadcode",
		ExternalID:        "sync-1",
	}
}

func TestSyncCreationNotReadyDoesNotCallProvider(t *testing.T) {
	store := newFakeStore()
	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := readyInput()
	// Break the develop match: no changed milestone file.
	in.Scan.ChangedFiles = nil

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionNotReady {
		t.Fatalf("expected not_ready, got %s (%s)", res.Action, res.Reason)
	}
	if len(provider.calls) != 0 {
		t.Fatalf("expected no provider calls, got %v", provider.calls)
	}
	if len(store.marked) != 0 || store.upserted {
		t.Fatalf("expected no ledger marks or upsert, marked=%v upserted=%v", store.marked, store.upserted)
	}
}

func TestSyncCreationReadyRunsStepsAndMarksLedger(t *testing.T) {
	store := newFakeStore()
	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	res, err := svc.SyncCreation(context.Background(), readyInput())
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionProjected {
		t.Fatalf("expected projected, got %s (%s)", res.Action, res.Reason)
	}

	wantProviderOrder := []string{"comment", "body", "status"}
	if !equalStrings(provider.calls, wantProviderOrder) {
		t.Fatalf("provider call order = %v, want %v", provider.calls, wantProviderOrder)
	}

	wantLedger := []storage.RoadmapSyncStep{
		storage.StepDevelopMatched,
		storage.StepOriginalCommentPreserved,
		storage.StepPlaneBodyUpdated,
		storage.StepPlaneTodoMoved,
	}
	if !equalSteps(store.marked, wantLedger) {
		t.Fatalf("ledger marks = %v, want %v", store.marked, wantLedger)
	}
	if res.NextStep != roadmapsync.StepPlaneTodoMoved {
		t.Fatalf("expected last step plane_todo_moved, got %s", res.NextStep)
	}
}

func TestSyncCreationRetrySkipsCompletedSteps(t *testing.T) {
	store := newFakeStore()
	// Identity already exists with the first two steps recorded.
	store.found = true
	store.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                string(roadmapsync.ShapeMilestone),
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-1",
		ProviderRevision:     nullableString(testRevision),
		RoadmapRevision:      nullableString(testRevision),
	}
	store.completed[storage.StepDevelopMatched] = true
	store.completed[storage.StepOriginalCommentPreserved] = true

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	res, err := svc.SyncCreation(context.Background(), readyInput())
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionProjected {
		t.Fatalf("expected projected, got %s (%s)", res.Action, res.Reason)
	}
	if store.upserted {
		t.Fatal("expected no upsert when identity already exists")
	}

	// Comment was already preserved; only body+status should run again.
	wantProviderOrder := []string{"body", "status"}
	if !equalStrings(provider.calls, wantProviderOrder) {
		t.Fatalf("provider call order = %v, want %v", provider.calls, wantProviderOrder)
	}
	wantLedger := []storage.RoadmapSyncStep{
		storage.StepPlaneBodyUpdated,
		storage.StepPlaneTodoMoved,
	}
	if !equalSteps(store.marked, wantLedger) {
		t.Fatalf("ledger marks = %v, want %v", store.marked, wantLedger)
	}
}

func TestSyncCreationStopsBeforeLaterStepOnProviderError(t *testing.T) {
	store := newFakeStore()
	bodyErr := errors.New("plane body projection failed")
	provider := &fakeProvider{failOn: "body", failErr: bodyErr}
	svc := NewService(store, provider, "")

	_, err := svc.SyncCreation(context.Background(), readyInput())
	if !errors.Is(err, bodyErr) {
		t.Fatalf("expected body error, got %v", err)
	}

	// Comment ran and was marked; body ran but failed; status must not run.
	wantProviderOrder := []string{"comment", "body"}
	if !equalStrings(provider.calls, wantProviderOrder) {
		t.Fatalf("provider call order = %v, want %v", provider.calls, wantProviderOrder)
	}
	// develop_matched + original_comment_preserved recorded; body never marked.
	wantLedger := []storage.RoadmapSyncStep{
		storage.StepDevelopMatched,
		storage.StepOriginalCommentPreserved,
	}
	if !equalSteps(store.marked, wantLedger) {
		t.Fatalf("ledger marks = %v, want %v", store.marked, wantLedger)
	}
}

func TestSyncCreationSkipsSelfMutation(t *testing.T) {
	store := newFakeStore()
	provider := &fakeProvider{}
	svc := NewService(store, provider, "nomadcode-bot")

	in := readyInput()
	in.Actor = "nomadcode-bot"

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionSkipSelfMutation {
		t.Fatalf("expected skip_self_mutation, got %s (%s)", res.Action, res.Reason)
	}
	if len(provider.calls) != 0 {
		t.Fatalf("expected no provider calls on self mutation, got %v", provider.calls)
	}
	if len(store.marked) != 0 {
		t.Fatalf("expected no ledger marks on self mutation, got %v", store.marked)
	}
}

func TestSyncCreationRevisionMismatchConflicts(t *testing.T) {
	store := newFakeStore()
	store.found = true
	store.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                string(roadmapsync.ShapeMilestone),
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-1",
		ProviderRevision:     nullableString("old-prov-rev"), // mismatch
		RoadmapRevision:      nullableString("old-road-rev"), // mismatch
	}

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	res, err := svc.SyncCreation(context.Background(), readyInput())
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionConflict {
		t.Fatalf("expected conflict action, got %s (%s)", res.Action, res.Reason)
	}
	if store.upserted {
		t.Fatal("expected no upsert on revision mismatch conflict")
	}
	if len(provider.calls) > 0 {
		t.Fatal("expected no provider calls on revision mismatch conflict")
	}
	if len(store.marked) > 0 {
		t.Fatal("expected no ledger marks on revision mismatch conflict")
	}
}

func TestSyncCreationAllowedAttributeUpdateAndReconcile(t *testing.T) {
	store := newFakeStore()
	store.found = true
	store.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                string(roadmapsync.ShapeMilestone),
		RoadmapMilestonePath: "agent-roadmap/phase/old-path/milestones/m1.md", // differs
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-OLD", // differs
		ProviderRevision:     nullableString(testRevision),
		RoadmapRevision:      nullableString(testRevision),
	}

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := readyInput()
	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionProjected {
		t.Fatalf("expected projected action, got %s (%s)", res.Action, res.Reason)
	}
	if !store.upserted {
		t.Fatal("expected upsert for allowed attribute update")
	}
	if store.identity.RoadmapMilestonePath != testMilestonePath {
		t.Errorf("expected path to be updated to %q, got %q", testMilestonePath, store.identity.RoadmapMilestonePath)
	}
	if store.identity.WorkItemID != "NOMAD-1" {
		t.Errorf("expected WorkItemID to be updated to NOMAD-1, got %q", store.identity.WorkItemID)
	}
}

func equalStrings(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func equalSteps(a, b []storage.RoadmapSyncStep) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func TestSyncCreationMissingActiveMilestoneCreatesWorkItemAndRecordsLedger(t *testing.T) {
	store := newFakeStore()
	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	// 1. Prepare input representing missing active milestone (expected identity without provider details)
	in := readyInput()
	in.Expected = roadmapsync.Identity{
		Shape:                roadmapsync.ShapeMilestone,
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Tenant:               "general",
		Project:              "nomad",
	}
	// Also clear Ref as there is no associated work item yet
	in.Ref = workitem.Ref{}

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}

	if res.Action != SyncActionProjected {
		t.Fatalf("expected projected action, got %s (%s)", res.Action, res.Reason)
	}

	// Fake creator should be called once with "create"
	wantCalls := []string{"create"}
	if !equalStrings(provider.calls, wantCalls) {
		t.Fatalf("provider calls = %v, want %v", provider.calls, wantCalls)
	}

	// Title should be `[milestone-id] title` (in our case extract title should return Milestone from Markdown)
	expectedTitle := "[m1] Milestone"
	if provider.createdInput.Title != expectedTitle {
		t.Errorf("expected created title %q, got %q", expectedTitle, provider.createdInput.Title)
	}

	// Ledger should upsert and mark StepDevelopMatched
	if !store.upserted {
		t.Fatal("expected identity to be upserted in store")
	}
	if store.identity.WorkItemID != "CREATED-123" {
		t.Errorf("expected upserted WorkItemID to be CREATED-123, got %q", store.identity.WorkItemID)
	}
	if store.identity.RoadmapItemID != "m1" {
		t.Errorf("expected upserted RoadmapItemID to be m1, got %q", store.identity.RoadmapItemID)
	}

	wantLedger := []storage.RoadmapSyncStep{
		storage.StepDevelopMatched,
	}
	if !equalSteps(store.marked, wantLedger) {
		t.Fatalf("ledger marks = %v, want %v", store.marked, wantLedger)
	}
	if res.NextStep != roadmapsync.StepDevelopMatched {
		t.Fatalf("expected next step to be develop_matched, got %s", res.NextStep)
	}
}

// TestSyncCreationBridgeProducedMissingCreate tests the actual bridge-produced
// missing-create job shape: Expected.Provider="plane", Tenant and Project set,
// WorkItemID empty, Ref.ID empty. This is the path that was broken when the
// predicate required Provider == "".
func TestSyncCreationBridgeProducedMissingCreate(t *testing.T) {
	store := newFakeStore()
	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	// Build input exactly as bridge.enqueueDoc does for missing-create:
	// Provider="plane", Tenant/Project set from config, WorkItemID/Ref empty.
	in := SyncCreationInput{
		Scan: roadmapsync.ScanResult{
			Revision:     roadmapsync.Revision{Branch: "develop", Revision: "bbbb"},
			ChangedFiles: []string{testMilestonePath},
			ScannedMilestones: []roadmapsync.ScannedMilestone{
				{Path: testMilestonePath},
			},
		},
		Expected: roadmapsync.Identity{
			Shape:                roadmapsync.ShapeMilestone,
			RoadmapMilestonePath: testMilestonePath,
			RoadmapItemID:        "m1",
			Provider:             "plane",
			Tenant:               "acme",
			Project:              "proj-1",
		},
		Ref:               workitem.Ref{},
		OriginalBody:      "",
		TodoStateID:       "todo-state",
		MilestoneMarkdown: "# Milestone\n\nbody",
		RoadmapRevision:   "bbbb",
	}

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}

	if res.Action != SyncActionProjected {
		t.Fatalf("expected projected action, got %s (%s)", res.Action, res.Reason)
	}

	// Verify CreateWorkItem was called exactly once.
	if !equalStrings(provider.calls, []string{"create"}) {
		t.Fatalf("provider calls = %v, want [create]", provider.calls)
	}

	// Verify CreateInput has the right provider/tenant/project.
	if provider.createdInput.Provider != "plane" {
		t.Errorf("createdInput.Provider = %q, want %q", provider.createdInput.Provider, "plane")
	}
	if provider.createdInput.Tenant != "acme" {
		t.Errorf("createdInput.Tenant = %q, want %q", provider.createdInput.Tenant, "acme")
	}
	if provider.createdInput.Project != "proj-1" {
		t.Errorf("createdInput.Project = %q, want %q", provider.createdInput.Project, "proj-1")
	}

	// Verify identity was upserted with the created work item id.
	if !store.upserted {
		t.Fatal("expected identity to be upserted in store")
	}
	if store.identity.WorkItemID != "CREATED-123" {
		t.Errorf("upserted WorkItemID = %q, want CREATED-123", store.identity.WorkItemID)
	}
	if store.identity.Provider != "plane" {
		t.Errorf("upserted Provider = %q, want plane", store.identity.Provider)
	}
	if store.identity.Tenant != "acme" {
		t.Errorf("upserted Tenant = %q, want acme", store.identity.Tenant)
	}
	if store.identity.Project != "proj-1" {
		t.Errorf("upserted Project = %q, want proj-1", store.identity.Project)
	}

	// Verify ledger mark.
	wantLedger := []storage.RoadmapSyncStep{
		storage.StepDevelopMatched,
	}
	if !equalSteps(store.marked, wantLedger) {
		t.Fatalf("ledger marks = %v, want %v", store.marked, wantLedger)
	}
}

// TestSyncCreationMissingCreateRequiresTenantAndProject ensures that a
// missing-create job without Tenant or Project is rejected before attempting
// CreateWorkItem.
func TestSyncCreationMissingCreateRequiresTenantAndProject(t *testing.T) {
	store := newFakeStore()
	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := SyncCreationInput{
		Expected: roadmapsync.Identity{
			Shape:                roadmapsync.ShapeMilestone,
			RoadmapMilestonePath: testMilestonePath,
			RoadmapItemID:        "m1",
			Provider:             "plane",
			Project:              "proj-1",
			// Tenant missing
		},
		Ref: workitem.Ref{},
	}

	_, err := svc.SyncCreation(context.Background(), in)
	if err == nil {
		t.Fatal("expected error for missing tenant, got nil")
	}
	if !equalStrings(provider.calls, nil) && len(provider.calls) != 0 {
		t.Fatalf("provider must not be called on validation error, got %v", provider.calls)
	}
}

func TestSyncCreationAllowedAttributeUpdateTenantAndProject(t *testing.T) {
	store := newFakeStore()
	store.found = true
	store.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                string(roadmapsync.ShapeMilestone),
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "old-tenant",  // differs
		Project:              "old-project", // differs
		WorkItemID:           "NOMAD-1",
		ProviderRevision:     nullableString(testRevision),
		RoadmapRevision:      nullableString(testRevision),
	}

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := readyInput()
	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionProjected {
		t.Fatalf("expected projected action, got %s (%s)", res.Action, res.Reason)
	}
	if !store.upserted {
		t.Fatal("expected upsert for allowed tenant/project update")
	}
	if store.identity.Tenant != "general" {
		t.Errorf("expected Tenant to be updated to general, got %q", store.identity.Tenant)
	}
	if store.identity.Project != "nomad" {
		t.Errorf("expected Project to be updated to nomad, got %q", store.identity.Project)
	}
}

func TestSyncCreationWorkItemConflictReturnsConflictAction(t *testing.T) {
	store := newFakeStore()
	store.found = true
	store.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                string(roadmapsync.ShapeMilestone),
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-1",
		ProviderRevision:     nullableString(testRevision),
		RoadmapRevision:      nullableString(testRevision),
	}
	// Simulate unique constraint violation error on upsert (e.g. work item bound to different milestone)
	store.upsertErr = storage.ErrRoadmapSyncIdentityConflict

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := readyInput()
	// Trigger attribute update by changing the path
	in.Scan.ChangedFiles = []string{"agent-roadmap/phase/p1/milestones/m-new.md"}
	in.Scan.ScannedMilestones[0].Path = "agent-roadmap/phase/p1/milestones/m-new.md"
	in.Expected.RoadmapMilestonePath = "agent-roadmap/phase/p1/milestones/m-new.md"

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionConflict {
		t.Fatalf("expected conflict action, got %s (%s)", res.Action, res.Reason)
	}
	if !strings.Contains(res.Reason, "identity conflict") {
		t.Errorf("expected conflict reason, got %q", res.Reason)
	}
}

func TestSyncCreationMissingCreateWorkItemConflictReturnsConflictAction(t *testing.T) {
	store := newFakeStore()
	store.upsertErr = storage.ErrRoadmapSyncIdentityConflict

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := readyInput()
	in.Expected = roadmapsync.Identity{
		Shape:                roadmapsync.ShapeMilestone,
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Tenant:               "general",
		Project:              "nomad",
	}
	in.Ref = workitem.Ref{}

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionConflict {
		t.Fatalf("expected conflict action, got %s (%s)", res.Action, res.Reason)
	}
}

func TestSyncCreationPreservesTriggerRefForProviderProjection(t *testing.T) {
	store := newFakeStore()
	store.found = true
	store.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                string(roadmapsync.ShapeMilestone),
		RoadmapMilestonePath: testMilestonePath,
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-OLD",
		ProviderRevision:     nullableString(testRevision),
		RoadmapRevision:      nullableString(testRevision),
	}
	// Simulate partial resume; say StepDevelopMatched and StepOriginalCommentPreserved are complete
	store.completed[storage.StepDevelopMatched] = true
	store.completed[storage.StepOriginalCommentPreserved] = true

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := readyInput()
	// Trigger carries WorkItemID = NOMAD-1
	in.Expected.WorkItemID = "NOMAD-1"
	in.Ref.ID = "NOMAD-1"
	in.Scan.ScannedMilestones[0].Identity.WorkItemID = "NOMAD-1"

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionProjected {
		t.Fatalf("expected projected action, got %s (%s)", res.Action, res.Reason)
	}

	// Verify upsert occurred and work item ID updated to NOMAD-1
	if !store.upserted {
		t.Fatal("expected upsert for attribute update")
	}
	if store.identity.WorkItemID != "NOMAD-1" {
		t.Errorf("expected upserted work item ID to be NOMAD-1, got %q", store.identity.WorkItemID)
	}

	// Verify provider calls targeted NOMAD-1
	// The remaining steps that run: plane_body_updated (body) and plane_todo_moved (status)
	wantCalls := []string{"body", "status"}
	if !equalStrings(provider.calls, wantCalls) {
		t.Fatalf("provider calls = %v, want %v", provider.calls, wantCalls)
	}
	for i, ref := range provider.refCalls {
		if ref.ID != "NOMAD-1" {
			t.Errorf("call[%d] targeted ref ID %q, want NOMAD-1", i, ref.ID)
		}
	}
}

func TestSyncCreationUpdateAttributesBeforeCompleteNoOp(t *testing.T) {
	store := newFakeStore()
	store.found = true
	store.identity = db.RoadmapSyncIdentity{
		ID:                   1,
		Shape:                string(roadmapsync.ShapeMilestone),
		RoadmapMilestonePath: "agent-roadmap/phase/old-path/milestones/m1.md",
		RoadmapItemID:        "m1",
		Provider:             "plane",
		Tenant:               "general",
		Project:              "nomad",
		WorkItemID:           "NOMAD-OLD",
		ProviderRevision:     nullableString(""),
		RoadmapRevision:      nullableString(""),
	}
	// All steps complete
	store.completed[storage.StepDevelopMatched] = true
	store.completed[storage.StepOriginalCommentPreserved] = true
	store.completed[storage.StepPlaneBodyUpdated] = true
	store.completed[storage.StepPlaneTodoMoved] = true

	provider := &fakeProvider{}
	svc := NewService(store, provider, "")

	in := readyInput()
	// Trigger carries updated attributes (different path, work item, and revision)
	in.Scan.ChangedFiles = []string{testMilestonePath}
	in.Scan.ScannedMilestones[0].Path = testMilestonePath
	in.Scan.ScannedMilestones[0].Identity.RoadmapMilestonePath = testMilestonePath
	in.Expected.RoadmapMilestonePath = testMilestonePath
	in.Expected.WorkItemID = "NOMAD-1"
	in.Ref.ID = "NOMAD-1"
	in.Scan.ScannedMilestones[0].Identity.WorkItemID = "NOMAD-1"
	in.ProviderRevision = testRevision
	in.RoadmapRevision = testRevision

	res, err := svc.SyncCreation(context.Background(), in)
	if err != nil {
		t.Fatalf("SyncCreation returned error: %v", err)
	}
	if res.Action != SyncActionComplete {
		t.Fatalf("expected complete action, got %s (%s)", res.Action, res.Reason)
	}

	// No provider calls should have run
	if len(provider.calls) > 0 {
		t.Fatalf("expected no provider calls, got %v", provider.calls)
	}

	// Verify upsert occurred and updated attributes
	if !store.upserted {
		t.Fatal("expected upsert even on ActionComplete if attributes changed")
	}
	if store.identity.RoadmapMilestonePath != testMilestonePath {
		t.Errorf("expected upserted path to be %q, got %q", testMilestonePath, store.identity.RoadmapMilestonePath)
	}
	if store.identity.WorkItemID != "NOMAD-1" {
		t.Errorf("expected upserted work item ID to be NOMAD-1, got %q", store.identity.WorkItemID)
	}
	if derefString(store.identity.RoadmapRevision) != testRevision {
		t.Errorf("expected upserted revision to be %q, got %q", testRevision, derefString(store.identity.RoadmapRevision))
	}
}
