package planmanage

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

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workflow/plan"
	planlang "github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

type repositoryStub struct {
	createPlan            func(context.Context, plan.CreatePlanParams) (plan.CreateResult, error)
	loadPlan              func(context.Context, string, string) (plan.Plan, error)
	listPlans             func(context.Context, plan.ListPlansParams) ([]plan.Plan, error)
	updatePlanMetadata    func(context.Context, plan.UpdatePlanMetadataParams) (plan.Plan, error)
	loadDraft             func(context.Context, string, string) (plan.PlanDraft, error)
	saveDraft             func(context.Context, plan.SaveDraftParams) (plan.PlanDraft, error)
	loadPublishReceipt    func(context.Context, string, string, plan.Actor, string) (plan.PlanVersion, bool, error)
	publishDraft          func(context.Context, plan.PublishDraftParams) (plan.PublishResult, error)
	loadVersion           func(context.Context, string, string, string) (plan.PlanVersion, error)
	resolveBindingVersion func(context.Context, string, string, string) (plan.PlanVersion, error)
	inspectBindingVersion func(context.Context, string, string, string) (plan.BindingVersionInspection, error)
	listVersions          func(context.Context, plan.ListVersionsParams) ([]plan.PlanVersionSummary, error)
	republishVersion      func(context.Context, plan.RepublishVersionParams) (plan.PublishResult, error)
	setDefaultVersion     func(context.Context, plan.SetDefaultVersionParams) (plan.Plan, error)
	archivePlan           func(context.Context, plan.ArchivePlanParams) (plan.Plan, error)
	restorePlan           func(context.Context, plan.RestorePlanParams) (plan.Plan, error)
}

func unexpected(operation string) error {
	return fmt.Errorf("unexpected repository call: %s", operation)
}

func (stub repositoryStub) CreatePlan(ctx context.Context, params plan.CreatePlanParams) (plan.CreateResult, error) {
	if stub.createPlan == nil {
		return plan.CreateResult{}, unexpected("CreatePlan")
	}
	return stub.createPlan(ctx, params)
}

func (stub repositoryStub) LoadPlan(ctx context.Context, workspaceID string, planID string) (plan.Plan, error) {
	if stub.loadPlan == nil {
		return plan.Plan{}, unexpected("LoadPlan")
	}
	return stub.loadPlan(ctx, workspaceID, planID)
}

func (stub repositoryStub) ListPlans(ctx context.Context, params plan.ListPlansParams) ([]plan.Plan, error) {
	if stub.listPlans == nil {
		return nil, unexpected("ListPlans")
	}
	return stub.listPlans(ctx, params)
}

func (stub repositoryStub) UpdatePlanMetadata(ctx context.Context, params plan.UpdatePlanMetadataParams) (plan.Plan, error) {
	if stub.updatePlanMetadata == nil {
		return plan.Plan{}, unexpected("UpdatePlanMetadata")
	}
	return stub.updatePlanMetadata(ctx, params)
}

func (stub repositoryStub) LoadDraft(ctx context.Context, workspaceID string, planID string) (plan.PlanDraft, error) {
	if stub.loadDraft == nil {
		return plan.PlanDraft{}, unexpected("LoadDraft")
	}
	return stub.loadDraft(ctx, workspaceID, planID)
}

func (stub repositoryStub) SaveDraft(ctx context.Context, params plan.SaveDraftParams) (plan.PlanDraft, error) {
	if stub.saveDraft == nil {
		return plan.PlanDraft{}, unexpected("SaveDraft")
	}
	return stub.saveDraft(ctx, params)
}

func (stub repositoryStub) LoadVersionByPublishIdempotency(ctx context.Context, workspaceID string, planID string, actor plan.Actor, key string) (plan.PlanVersion, bool, error) {
	if stub.loadPublishReceipt == nil {
		return plan.PlanVersion{}, false, nil
	}
	return stub.loadPublishReceipt(ctx, workspaceID, planID, actor, key)
}

func (stub repositoryStub) PublishDraft(ctx context.Context, params plan.PublishDraftParams) (plan.PublishResult, error) {
	if stub.publishDraft == nil {
		return plan.PublishResult{}, unexpected("PublishDraft")
	}
	return stub.publishDraft(ctx, params)
}

func (stub repositoryStub) LoadVersionByID(ctx context.Context, workspaceID string, planID string, versionID string) (plan.PlanVersion, error) {
	if stub.loadVersion == nil {
		return plan.PlanVersion{}, unexpected("LoadVersionByID")
	}
	return stub.loadVersion(ctx, workspaceID, planID, versionID)
}

func (stub repositoryStub) ResolveVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (plan.PlanVersion, error) {
	if stub.resolveBindingVersion == nil {
		return plan.PlanVersion{}, unexpected("ResolveVersionForBinding")
	}
	return stub.resolveBindingVersion(ctx, workspaceID, planID, versionID)
}

func (stub repositoryStub) InspectVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (plan.BindingVersionInspection, error) {
	if stub.inspectBindingVersion == nil {
		return plan.BindingVersionInspection{}, unexpected("InspectVersionForBinding")
	}
	return stub.inspectBindingVersion(ctx, workspaceID, planID, versionID)
}

func (stub repositoryStub) ListVersions(ctx context.Context, params plan.ListVersionsParams) ([]plan.PlanVersionSummary, error) {
	if stub.listVersions == nil {
		return nil, unexpected("ListVersions")
	}
	return stub.listVersions(ctx, params)
}

func (stub repositoryStub) RepublishVersion(ctx context.Context, params plan.RepublishVersionParams) (plan.PublishResult, error) {
	if stub.republishVersion == nil {
		return plan.PublishResult{}, unexpected("RepublishVersion")
	}
	return stub.republishVersion(ctx, params)
}

func (stub repositoryStub) SetDefaultVersion(ctx context.Context, params plan.SetDefaultVersionParams) (plan.Plan, error) {
	if stub.setDefaultVersion == nil {
		return plan.Plan{}, unexpected("SetDefaultVersion")
	}
	return stub.setDefaultVersion(ctx, params)
}

func (stub repositoryStub) ArchivePlan(ctx context.Context, params plan.ArchivePlanParams) (plan.Plan, error) {
	if stub.archivePlan == nil {
		return plan.Plan{}, unexpected("ArchivePlan")
	}
	return stub.archivePlan(ctx, params)
}

func (stub repositoryStub) RestorePlan(ctx context.Context, params plan.RestorePlanParams) (plan.Plan, error) {
	if stub.restorePlan == nil {
		return plan.Plan{}, unexpected("RestorePlan")
	}
	return stub.restorePlan(ctx, params)
}

type fixedClock time.Time

func (clock fixedClock) Now() time.Time { return time.Time(clock) }

func TestServiceCreateRequiresCompleteCompilation(t *testing.T) {
	now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
	compileCalled := false
	createCalled := false
	repository := repositoryStub{createPlan: func(_ context.Context, params plan.CreatePlanParams) (plan.CreateResult, error) {
		createCalled = true
		require.Equal(t, "wsp_1", params.WorkspaceID)
		require.Equal(t, "deploy_guard", params.PlanKey)
		require.Equal(t, "Deploy guard", params.DisplayName)
		require.Equal(t, simplePlanSource, params.Source)
		require.JSONEq(t, `{"nodes":{"a":{"x":1}},"zoom":2}`, string(params.Layout))
		require.Equal(t, "request-1", params.IdempotencyKey)
		require.Equal(t, now, params.Now)
		return plan.CreateResult{
			Plan:  plan.Plan{ID: "pln_1", WorkspaceID: params.WorkspaceID, PlanKey: params.PlanKey, DisplayName: params.DisplayName, State: plan.PlanStateActive, Revision: 1, CreatedBy: params.Actor, UpdatedBy: params.Actor, CreatedAt: now, UpdatedAt: now},
			Draft: plan.PlanDraft{PlanID: "pln_1", WorkspaceID: params.WorkspaceID, Source: params.Source, Layout: params.Layout, Revision: 1, UpdatedBy: params.Actor, CreatedAt: now, UpdatedAt: now},
		}, nil
	}}
	service := New(repository, WithClock(fixedClock(now)), withCompiler(func(source []byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
		compileCalled = true
		return planlang.Compile(source)
	}))

	plan, draft, err := service.Create(context.Background(), CreateInput{
		WorkspaceID: " wsp_1 ", Key: " deploy_guard ", DisplayName: " Deploy guard ",
		Source: simplePlanSource, Layout: json.RawMessage(`{"zoom":2,"nodes":{"a":{"x":1}}}`),
		Actor: Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, IdempotencyKey: " request-1 ",
	})
	require.NoError(t, err)
	require.True(t, compileCalled)
	require.True(t, createCalled)
	require.Equal(t, "pln_1", plan.ID)
	require.Equal(t, int64(1), draft.Revision)

	createCalled = false
	_, _, err = service.Create(context.Background(), CreateInput{
		WorkspaceID: "wsp_1", Key: "invalid", DisplayName: "Invalid",
		Source: "not valid yaml yet",
		Actor:  Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, IdempotencyKey: "request-invalid",
	})
	require.ErrorIs(t, err, storeerr.ErrUnprocessable)
	code, _, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeCompileFailed, code)
	require.False(t, createCalled)
}

func TestServiceSaveDraftCompilesAndPreservesOmittedLayout(t *testing.T) {
	now := time.Date(2026, 7, 20, 10, 15, 0, 0, time.UTC)
	saveCalled := false
	repository := repositoryStub{saveDraft: func(_ context.Context, params plan.SaveDraftParams) (plan.PlanDraft, error) {
		saveCalled = true
		require.True(t, params.PreserveLayout)
		require.Empty(t, params.Layout)
		require.Equal(t, simplePlanSource, params.Source)
		return plan.PlanDraft{
			PlanID: params.PlanID, WorkspaceID: params.WorkspaceID,
			Source: params.Source, Layout: []byte(`{"nodes":{"kept":true}}`),
			Revision: params.ExpectedRevision + 1, UpdatedBy: params.Actor,
			CreatedAt: now, UpdatedAt: now,
		}, nil
	}}
	service := New(repository, WithClock(fixedClock(now)))
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}

	draft, err := service.SaveDraft(context.Background(), SaveDraftInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedRevision: 4,
		Source: simplePlanSource, Actor: actor,
	})
	require.NoError(t, err)
	require.True(t, saveCalled)
	require.Equal(t, int64(5), draft.Revision)
	require.JSONEq(t, `{"nodes":{"kept":true}}`, string(draft.Layout))

	saveCalled = false
	_, err = service.SaveDraft(context.Background(), SaveDraftInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedRevision: 5,
		Source: "invalid source", Actor: actor,
	})
	require.ErrorIs(t, err, storeerr.ErrUnprocessable)
	require.False(t, saveCalled)
}

func TestServiceRejectsSourceBudgetBeforeRepositoryOrCompiler(t *testing.T) {
	t.Parallel()

	oversized := strings.Repeat("s", planlang.MaxSourceBytes+1)
	compileCalled := false
	service := New(repositoryStub{}, withCompiler(func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
		compileCalled = true
		return nil, nil, nil
	}))
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}

	_, _, err := service.Create(context.Background(), CreateInput{
		WorkspaceID: "wsp_1", Key: "budgeted", DisplayName: "Budgeted",
		Source: oversized, Actor: actor, IdempotencyKey: "create-budgeted",
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	_, err = service.SaveDraft(context.Background(), SaveDraftInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedRevision: 1,
		Source: oversized, Actor: actor,
	})
	require.ErrorIs(t, err, storeerr.ErrBadRequest)

	validation := service.Validate(context.Background(), oversized)
	foundBudgetDiagnostic := false
	for _, diagnostic := range validation.Diagnostics {
		foundBudgetDiagnostic = foundBudgetDiagnostic || diagnostic.Code == "resource.source_too_large"
	}
	require.True(t, foundBudgetDiagnostic, validation.Diagnostics)
	require.False(t, compileCalled)
}

func TestServiceValidateAndPublishExactDraftRevision(t *testing.T) {
	now := time.Date(2026, 7, 20, 10, 30, 0, 0, time.UTC)
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	var published plan.PublishDraftParams
	repository := repositoryStub{
		loadPlan: func(context.Context, string, string) (plan.Plan, error) {
			return plan.Plan{ID: "pln_1", State: plan.PlanStateActive, Revision: 7}, nil
		},
		loadDraft: func(_ context.Context, workspaceID string, planID string) (plan.PlanDraft, error) {
			require.Equal(t, "wsp_1", workspaceID)
			require.Equal(t, "pln_1", planID)
			return plan.PlanDraft{WorkspaceID: workspaceID, PlanID: planID, Revision: 4, Source: simplePlanSource}, nil
		},
		publishDraft: func(_ context.Context, params plan.PublishDraftParams) (plan.PublishResult, error) {
			published = params
			revision := params.DraftRevision
			return plan.PublishResult{
				Plan:    plan.Plan{ID: params.PlanID, WorkspaceID: params.WorkspaceID, PlanKey: "plan", DisplayName: "Plan", State: plan.PlanStateActive, Revision: 8, CreatedBy: params.Actor, UpdatedBy: params.Actor},
				Version: plan.PlanVersion{ID: "plv_1", WorkspaceID: params.WorkspaceID, PlanID: params.PlanID, DraftRevision: &revision, Source: simplePlanSource, Layout: []byte(`{}`), Artifact: params.Artifact, CompilerVersion: params.CompilerVersion, Digest: params.Digest, PublishedBy: params.Actor, PublishedAt: now},
			}, nil
		},
	}
	service := New(repository, WithClock(fixedClock(now)))

	validation := service.Validate(context.Background(), simplePlanSource)
	require.Empty(t, validation.Diagnostics)
	require.NotEmpty(t, validation.Artifact)
	require.NotEmpty(t, validation.Digest)

	plan, version, err := service.Publish(context.Background(), PublishInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedPlanRevision: 7, DraftRevision: 4,
		MakeDefault: true, Actor: actor, IdempotencyKey: "publish-1",
	})
	require.NoError(t, err)
	require.Equal(t, int64(8), plan.Revision)
	require.Equal(t, "plv_1", version.ID)
	require.Equal(t, planlang.CompilerVersion, published.CompilerVersion)
	require.Equal(t, validation.Digest, published.Digest)
	require.JSONEq(t, string(validation.Artifact), string(published.Artifact))
	require.Equal(t, int64(7), published.ExpectedPlanRevision)
	require.Equal(t, int64(4), published.DraftRevision)
	require.True(t, published.MakeDefault)
	require.Equal(t, now, published.Now)
}

func TestServiceValidateDraftLoadsExactStoredRevision(t *testing.T) {
	const storedSource = "stored draft source"
	compileCalled := false
	service := New(repositoryStub{
		loadDraft: func(_ context.Context, workspaceID string, planID string) (plan.PlanDraft, error) {
			require.Equal(t, "wsp_1", workspaceID)
			require.Equal(t, "pln_1", planID)
			// No Plan load is configured: validating an archived Plan's preserved
			// draft is intentionally a read and must not consult lifecycle state.
			return plan.PlanDraft{
				WorkspaceID: workspaceID,
				PlanID:      planID,
				Revision:    4,
				Source:      storedSource,
			}, nil
		},
	}, withCompiler(func(source []byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
		compileCalled = true
		require.Equal(t, storedSource, string(source))
		return nil, []planlang.Diagnostic{{
			Severity: planlang.SeverityError,
			Code:     "test.stored_source",
			Path:     "$",
		}}, nil
	}))

	validation, err := service.ValidateDraft(context.Background(), " wsp_1 ", " pln_1 ", 4)
	require.NoError(t, err)
	require.True(t, compileCalled)
	require.Len(t, validation.Diagnostics, 1)
	require.Equal(t, "test.stored_source", validation.Diagnostics[0].Code)
}

func TestServiceValidateDraftRejectsStaleAndMissingDraft(t *testing.T) {
	t.Run("stale revision has structured details", func(t *testing.T) {
		compileCalled := false
		service := New(repositoryStub{
			loadDraft: func(context.Context, string, string) (plan.PlanDraft, error) {
				return plan.PlanDraft{Revision: 7, Source: simplePlanSource}, nil
			},
		}, withCompiler(func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
			compileCalled = true
			return nil, nil, nil
		}))

		_, err := service.ValidateDraft(context.Background(), "wsp_1", "pln_1", 6)
		require.ErrorIs(t, err, storeerr.ErrConflict)
		code, details, ok := ErrorDetails(err)
		require.True(t, ok)
		require.Equal(t, ErrorCodeDraftConflict, code)
		require.Equal(t, RevisionConflictDetails{
			Resource:         "planDraft",
			ExpectedRevision: 6,
			CurrentRevision:  7,
		}, details)
		require.False(t, compileCalled)
	})

	t.Run("missing or cross-workspace draft is not found", func(t *testing.T) {
		service := New(repositoryStub{
			loadDraft: func(_ context.Context, workspaceID string, planID string) (plan.PlanDraft, error) {
				require.Equal(t, "wsp_other", workspaceID)
				require.Equal(t, "pln_1", planID)
				return plan.PlanDraft{}, storeerr.NotFound("workflow Plan not found")
			},
		})

		_, err := service.ValidateDraft(context.Background(), "wsp_other", "pln_1", 1)
		require.ErrorIs(t, err, storeerr.ErrNotFound)
		code, _, ok := ErrorDetails(err)
		require.True(t, ok)
		require.Equal(t, ErrorCodeNotFound, code)
	})
}

func TestServicePlanRevisionPrecheckHasStructuredDetails(t *testing.T) {
	service := New(repositoryStub{
		loadPlan: func(context.Context, string, string) (plan.Plan, error) {
			return plan.Plan{ID: "pln_1", State: plan.PlanStateActive, Revision: 9}, nil
		},
	})

	_, _, err := service.Publish(context.Background(), PublishInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedPlanRevision: 8, DraftRevision: 4,
		Actor: Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, IdempotencyKey: "publish-1",
	})
	require.ErrorIs(t, err, storeerr.ErrConflict)
	code, details, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeRevisionConflict, code)
	require.Equal(t, RevisionConflictDetails{
		Resource:         "plan",
		ExpectedRevision: 8,
		CurrentRevision:  9,
	}, details)
}

func TestServicePublishRejectsStaleOrInvalidDraftBeforeMutation(t *testing.T) {
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	for _, test := range []struct {
		name       string
		draft      plan.PlanDraft
		wantCode   ErrorCode
		wantStatus error
	}{
		{name: "stale", draft: plan.PlanDraft{Revision: 9, Source: simplePlanSource}, wantCode: ErrorCodeDraftConflict, wantStatus: storeerr.ErrConflict},
		{name: "invalid", draft: plan.PlanDraft{Revision: 4, Source: "apiVersion: wrong"}, wantCode: ErrorCodeCompileFailed, wantStatus: storeerr.ErrUnprocessable},
	} {
		t.Run(test.name, func(t *testing.T) {
			publishCalled := false
			service := New(repositoryStub{
				loadPlan: func(context.Context, string, string) (plan.Plan, error) {
					return plan.Plan{ID: "pln_1", State: plan.PlanStateActive, Revision: 3}, nil
				},
				loadDraft: func(context.Context, string, string) (plan.PlanDraft, error) { return test.draft, nil },
				publishDraft: func(context.Context, plan.PublishDraftParams) (plan.PublishResult, error) {
					publishCalled = true
					return plan.PublishResult{}, nil
				},
			})
			_, _, err := service.Publish(context.Background(), PublishInput{
				WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedPlanRevision: 3, DraftRevision: 4,
				Actor: actor, IdempotencyKey: "publish-1",
			})
			require.ErrorIs(t, err, test.wantStatus)
			code, _, ok := ErrorDetails(err)
			require.True(t, ok)
			require.Equal(t, test.wantCode, code)
			require.False(t, publishCalled)
		})
	}
}

func TestServicePublishReplayUsesCommittedArtifactAfterDraftMoves(t *testing.T) {
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	committed := plan.PlanVersion{
		ID: "plv_1", DraftRevision: int64Pointer(4), Source: simplePlanSource,
		Artifact: []byte(`{"artifactVersion":"old"}`), CompilerVersion: "compiler/old", Digest: "sha256:old",
		PublishedBy: storeActor(actor),
	}
	loadDraftCalled, compileCalled := false, false
	repository := repositoryStub{
		loadPublishReceipt: func(_ context.Context, _, _ string, gotActor plan.Actor, key string) (plan.PlanVersion, bool, error) {
			require.Equal(t, storeActor(actor), gotActor)
			require.Equal(t, "publish-1", key)
			return committed, true, nil
		},
		loadDraft: func(context.Context, string, string) (plan.PlanDraft, error) {
			loadDraftCalled = true
			return plan.PlanDraft{Revision: 99, Source: "new draft"}, nil
		},
		publishDraft: func(_ context.Context, params plan.PublishDraftParams) (plan.PublishResult, error) {
			require.Equal(t, committed.Artifact, params.Artifact)
			require.Equal(t, committed.CompilerVersion, params.CompilerVersion)
			require.Equal(t, committed.Digest, params.Digest)
			return plan.PublishResult{
				Plan:    plan.Plan{ID: "pln_1", State: plan.PlanStateActive, Revision: 8},
				Version: committed, Replayed: true,
			}, nil
		},
	}
	service := New(repository, withCompiler(func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
		compileCalled = true
		return nil, nil, nil
	}))

	_, version, err := service.Publish(context.Background(), PublishInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedPlanRevision: 7, DraftRevision: 4,
		Actor: actor, IdempotencyKey: "publish-1",
	})
	require.NoError(t, err)
	require.Equal(t, committed.ID, version.ID)
	require.False(t, loadDraftCalled)
	require.False(t, compileCalled)
}

func TestServicePublishRejectsArchivedPlanBeforeReadingOrCompilingDraft(t *testing.T) {
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	draftCalled, compileCalled := false, false
	service := New(repositoryStub{
		loadPlan: func(context.Context, string, string) (plan.Plan, error) {
			return plan.Plan{ID: "pln_1", State: plan.PlanStateArchived, Revision: 6}, nil
		},
		loadDraft: func(context.Context, string, string) (plan.PlanDraft, error) {
			draftCalled = true
			return plan.PlanDraft{Revision: 3, Source: "invalid"}, nil
		},
	}, withCompiler(func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
		compileCalled = true
		return nil, nil, nil
	}))

	_, _, err := service.Publish(context.Background(), PublishInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedPlanRevision: 6, DraftRevision: 3,
		Actor: actor, IdempotencyKey: "publish-archived",
	})
	require.ErrorIs(t, err, storeerr.ErrConflict)
	code, _, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeArchived, code)
	require.False(t, draftCalled)
	require.False(t, compileCalled)
}

func TestServiceRepublishCompilesImmutableHistoricalSource(t *testing.T) {
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	var captured plan.RepublishVersionParams
	repository := repositoryStub{
		loadPlan: func(context.Context, string, string) (plan.Plan, error) {
			return plan.Plan{ID: "pln_1", State: plan.PlanStateActive, Revision: 11}, nil
		},
		loadVersion: func(_ context.Context, _, _ string, versionID string) (plan.PlanVersion, error) {
			require.Equal(t, "plv_old", versionID)
			return plan.PlanVersion{ID: versionID, Source: simplePlanSource}, nil
		},
		republishVersion: func(_ context.Context, params plan.RepublishVersionParams) (plan.PublishResult, error) {
			captured = params
			sourceID := "plv_old"
			return plan.PublishResult{
				Plan:    plan.Plan{ID: params.PlanID, WorkspaceID: params.WorkspaceID, State: plan.PlanStateActive, Revision: 12},
				Version: plan.PlanVersion{ID: "plv_new", WorkspaceID: params.WorkspaceID, PlanID: params.PlanID, SourceVersionID: &sourceID, Source: simplePlanSource, Artifact: params.Artifact, CompilerVersion: params.CompilerVersion, Digest: params.Digest, PublishedBy: params.Actor},
			}, nil
		},
	}
	service := New(repository)
	_, version, err := service.Republish(context.Background(), RepublishInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", SourceVersionID: "plv_old", ExpectedPlanRevision: 11,
		MakeDefault: true, Actor: actor, IdempotencyKey: "rollback-1",
	})
	require.NoError(t, err)
	require.Equal(t, "plv_old", captured.SourceVersionID)
	require.Equal(t, int64(11), captured.ExpectedPlanRevision)
	require.True(t, captured.MakeDefault)
	require.NotEmpty(t, captured.Artifact)
	require.NotNil(t, version.SourceVersionID)
	require.Equal(t, "plv_old", *version.SourceVersionID)
}

func TestServiceRepublishReplayDoesNotDependOnCurrentCompiler(t *testing.T) {
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	committed := plan.PlanVersion{
		ID: "plv_3", SourceVersionID: stringPointer("plv_1"),
		Artifact: []byte(`{"artifactVersion":"old"}`), CompilerVersion: "compiler/old", Digest: "sha256:old",
		PublishedBy: storeActor(actor),
	}
	loadOriginCalled, compileCalled := false, false
	service := New(repositoryStub{
		loadPublishReceipt: func(context.Context, string, string, plan.Actor, string) (plan.PlanVersion, bool, error) {
			return committed, true, nil
		},
		loadVersion: func(context.Context, string, string, string) (plan.PlanVersion, error) {
			loadOriginCalled = true
			return plan.PlanVersion{}, nil
		},
		republishVersion: func(_ context.Context, params plan.RepublishVersionParams) (plan.PublishResult, error) {
			require.Equal(t, committed.Artifact, params.Artifact)
			require.Equal(t, committed.CompilerVersion, params.CompilerVersion)
			require.Equal(t, committed.Digest, params.Digest)
			return plan.PublishResult{
				Plan:    plan.Plan{ID: "pln_1", State: plan.PlanStateActive, Revision: 9},
				Version: committed, Replayed: true,
			}, nil
		},
	}, withCompiler(func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
		compileCalled = true
		return nil, nil, nil
	}))

	_, version, err := service.Republish(context.Background(), RepublishInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", SourceVersionID: "plv_1", ExpectedPlanRevision: 8,
		Actor: actor, IdempotencyKey: "rollback-1",
	})
	require.NoError(t, err)
	require.Equal(t, committed.ID, version.ID)
	require.False(t, loadOriginCalled)
	require.False(t, compileCalled)
}

func TestServiceListUsesOpaqueStableCursorAndLookahead(t *testing.T) {
	updatedAt := time.Date(2026, 7, 20, 11, 0, 0, 0, time.UTC)
	calls := 0
	repository := repositoryStub{listPlans: func(_ context.Context, params plan.ListPlansParams) ([]plan.Plan, error) {
		calls++
		require.Equal(t, 3, params.Limit)
		if calls == 1 {
			require.Nil(t, params.BeforeUpdatedAt)
			return []plan.Plan{
				{ID: "pln_3", UpdatedAt: updatedAt.Add(2 * time.Minute)},
				{ID: "pln_2", UpdatedAt: updatedAt.Add(time.Minute)},
				{ID: "pln_1", UpdatedAt: updatedAt},
			}, nil
		}
		require.NotNil(t, params.BeforeUpdatedAt)
		require.Equal(t, updatedAt.Add(time.Minute), *params.BeforeUpdatedAt)
		require.Equal(t, "pln_2", params.BeforeID)
		return nil, nil
	}}
	service := New(repository)
	first, err := service.List(context.Background(), ListInput{WorkspaceID: "wsp_1", Limit: 2})
	require.NoError(t, err)
	require.Len(t, first.Items, 2)
	require.NotEmpty(t, first.NextCursor)
	second, err := service.List(context.Background(), ListInput{WorkspaceID: "wsp_1", Limit: 2, Cursor: first.NextCursor})
	require.NoError(t, err)
	require.Empty(t, second.Items)
	require.Empty(t, second.NextCursor)
}

func TestServiceListVersionsDistinguishesMissingPlanFromEmptyHistory(t *testing.T) {
	t.Run("missing parent", func(t *testing.T) {
		listCalled := false
		service := New(repositoryStub{
			loadPlan: func(context.Context, string, string) (plan.Plan, error) {
				return plan.Plan{}, storeerr.NotFound("workflow Plan not found")
			},
			listVersions: func(context.Context, plan.ListVersionsParams) ([]plan.PlanVersionSummary, error) {
				listCalled = true
				return nil, nil
			},
		})

		_, err := service.ListVersions(context.Background(), "wsp_1", "pln_missing", 20, "")
		require.ErrorIs(t, err, storeerr.ErrNotFound)
		code, _, ok := ErrorDetails(err)
		require.True(t, ok)
		require.Equal(t, ErrorCodeNotFound, code)
		require.False(t, listCalled)
	})

	t.Run("existing parent without versions", func(t *testing.T) {
		service := New(repositoryStub{
			loadPlan: func(_ context.Context, workspaceID, planID string) (plan.Plan, error) {
				require.Equal(t, "wsp_1", workspaceID)
				require.Equal(t, "pln_1", planID)
				return plan.Plan{ID: planID, WorkspaceID: workspaceID}, nil
			},
			listVersions: func(_ context.Context, params plan.ListVersionsParams) ([]plan.PlanVersionSummary, error) {
				require.Equal(t, "wsp_1", params.WorkspaceID)
				require.Equal(t, "pln_1", params.PlanID)
				return []plan.PlanVersionSummary{}, nil
			},
		})

		page, err := service.ListVersions(context.Background(), "wsp_1", "pln_1", 20, "")
		require.NoError(t, err)
		require.Empty(t, page.Items)
		require.Empty(t, page.NextCursor)
	})

	t.Run("summary projection and cursor", func(t *testing.T) {
		publishedAt := time.Date(2026, time.July, 20, 10, 0, 0, 0, time.FixedZone("test", 8*60*60))
		draftRevision := int64(7)
		originID := "plv_origin"
		service := New(repositoryStub{
			loadPlan: func(_ context.Context, workspaceID, planID string) (plan.Plan, error) {
				return plan.Plan{ID: planID, WorkspaceID: workspaceID}, nil
			},
			listVersions: func(_ context.Context, params plan.ListVersionsParams) ([]plan.PlanVersionSummary, error) {
				require.Equal(t, 3, params.Limit)
				return []plan.PlanVersionSummary{
					{
						ID: "plv_3", WorkspaceID: params.WorkspaceID, PlanID: params.PlanID,
						DraftRevision: &draftRevision, CompilerVersion: "compiler-v1", Digest: "digest-3",
						PublishedBy: plan.Actor{Type: plan.ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"},
						PublishedAt: publishedAt,
					},
					{
						ID: "plv_2", WorkspaceID: params.WorkspaceID, PlanID: params.PlanID,
						SourceVersionID: &originID, CompilerVersion: "compiler-v1", Digest: "digest-2",
						PublishedBy: plan.Actor{Type: plan.ActorTypeAgent, ID: "agt_1", MemberID: "mem_1"},
						PublishedAt: publishedAt.Add(-time.Minute),
					},
					{ID: "plv_1", WorkspaceID: params.WorkspaceID, PlanID: params.PlanID},
				}, nil
			},
		})

		page, err := service.ListVersions(context.Background(), "wsp_1", "pln_1", 2, "")
		require.NoError(t, err)
		require.Len(t, page.Items, 2)
		require.NotEmpty(t, page.NextCursor)
		require.Equal(t, VersionSummary{
			ID: "plv_3", WorkspaceID: "wsp_1", PlanID: "pln_1",
			DraftRevision: &draftRevision, CompilerVersion: "compiler-v1", Digest: "digest-3",
			PublishedBy: Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, PublishedAt: publishedAt,
		}, page.Items[0])
		require.Equal(t, &originID, page.Items[1].SourceVersionID)
	})
}

func TestServiceMapsRepositoryLifecycleAndCASFailures(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		wantCode ErrorCode
		wantKind error
	}{
		{name: "draft revision", err: plan.RevisionConflictError{Resource: "plan draft", Expected: 1, Actual: 2}, wantCode: ErrorCodeDraftConflict, wantKind: storeerr.ErrConflict},
		{name: "plan revision", err: plan.RevisionConflictError{Resource: "plan", Expected: 1, Actual: 2}, wantCode: ErrorCodeRevisionConflict, wantKind: storeerr.ErrConflict},
		{name: "idempotency", err: plan.IdempotencyConflictError{Operation: "publish"}, wantCode: ErrorCodeIdempotencyConflict, wantKind: storeerr.ErrConflict},
		{name: "archived", err: plan.ErrArchived, wantCode: ErrorCodeArchived, wantKind: storeerr.ErrConflict},
		{name: "not found", err: storeerr.NotFound("missing"), wantCode: ErrorCodeNotFound, wantKind: storeerr.ErrNotFound},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			mapped := mapRepositoryError(test.err)
			require.ErrorIs(t, mapped, test.wantKind)
			code, details, ok := ErrorDetails(mapped)
			require.True(t, ok)
			require.Equal(t, test.wantCode, code)
			switch test.wantCode {
			case ErrorCodeDraftConflict:
				require.Equal(t, RevisionConflictDetails{Resource: "planDraft", ExpectedRevision: 1, CurrentRevision: 2}, details)
			case ErrorCodeRevisionConflict:
				require.Equal(t, RevisionConflictDetails{Resource: "plan", ExpectedRevision: 1, CurrentRevision: 2}, details)
			}
		})
	}
}

func TestServiceValidateTurnsInternalCompilerFailureIntoStableDiagnostic(t *testing.T) {
	for _, test := range []struct {
		name    string
		compile compiler
	}{
		{
			name: "compiler error",
			compile: func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
				return nil, nil, errors.New("encoder detail that must not leak")
			},
		},
		{
			name: "missing artifact without diagnostics",
			compile: func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
				return nil, nil, nil
			},
		},
		{
			name: "invalid artifact envelope",
			compile: func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
				return &planlang.Artifact{
					ArtifactVersion: planlang.ArtifactVersion,
					CompilerVersion: planlang.CompilerVersion,
					APIVersion:      planlang.APIVersion,
					Digest:          "sha256:tampered",
				}, nil, nil
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			service := New(repositoryStub{}, withCompiler(test.compile))
			result := service.Validate(context.Background(), simplePlanSource)
			require.Len(t, result.Diagnostics, 1)
			require.Equal(t, "compiler.internal", result.Diagnostics[0].Code)
			require.NotContains(t, result.Diagnostics[0].Message, "encoder detail")
			require.Empty(t, result.Artifact)
			require.Empty(t, result.Digest)
		})
	}
}

func TestServicePublishRejectsInvalidCompilerArtifactBeforeRepositoryMutation(t *testing.T) {
	publishCalled := false
	service := New(repositoryStub{
		loadPlan: func(context.Context, string, string) (plan.Plan, error) {
			return plan.Plan{ID: "pln_1", State: plan.PlanStateActive, Revision: 1}, nil
		},
		loadDraft: func(context.Context, string, string) (plan.PlanDraft, error) {
			return plan.PlanDraft{PlanID: "pln_1", Revision: 1, Source: simplePlanSource}, nil
		},
		publishDraft: func(context.Context, plan.PublishDraftParams) (plan.PublishResult, error) {
			publishCalled = true
			return plan.PublishResult{}, nil
		},
	}, withCompiler(func([]byte) (*planlang.Artifact, []planlang.Diagnostic, error) {
		return &planlang.Artifact{
			ArtifactVersion: planlang.ArtifactVersion,
			CompilerVersion: planlang.CompilerVersion,
			APIVersion:      planlang.APIVersion,
			Digest:          "sha256:tampered",
		}, nil, nil
	}))

	_, _, err := service.Publish(context.Background(), PublishInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", ExpectedPlanRevision: 1, DraftRevision: 1,
		Actor: Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, IdempotencyKey: "publish-invalid-artifact",
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "verify workflow Plan artifact")
	require.False(t, publishCalled)
}

func TestServiceResolveVersionForBindingReturnsExactVerifiedArtifact(t *testing.T) {
	artifact, diagnostics, err := planlang.Compile([]byte(simplePlanSource))
	require.NoError(t, err)
	require.False(t, planlang.HasErrors(diagnostics), diagnostics)
	payload, err := artifact.DeterministicJSON()
	require.NoError(t, err)

	repository := repositoryStub{resolveBindingVersion: func(_ context.Context, workspaceID string, planID string, versionID string) (plan.PlanVersion, error) {
		require.Equal(t, "wsp_1", workspaceID)
		require.Equal(t, "pln_1", planID)
		require.Equal(t, "plv_1", versionID)
		return plan.PlanVersion{
			ID: "plv_1", WorkspaceID: "wsp_1", PlanID: "pln_1",
			Artifact: payload, CompilerVersion: artifact.CompilerVersion, Digest: artifact.Digest,
		}, nil
	}}
	resolved, err := New(repository).ResolveVersionForBinding(context.Background(), " wsp_1 ", " pln_1 ", " plv_1 ")
	require.NoError(t, err)
	require.Equal(t, "plv_1", resolved.Version.ID)
	require.NotNil(t, resolved.Artifact)
	require.Equal(t, artifact.Digest, resolved.Artifact.Digest)
}

func TestServiceResolveVersionForBindingRejectsMetadataMismatch(t *testing.T) {
	artifact, diagnostics, err := planlang.Compile([]byte(simplePlanSource))
	require.NoError(t, err)
	require.False(t, planlang.HasErrors(diagnostics), diagnostics)
	payload, err := artifact.DeterministicJSON()
	require.NoError(t, err)

	service := New(repositoryStub{resolveBindingVersion: func(context.Context, string, string, string) (plan.PlanVersion, error) {
		return plan.PlanVersion{
			ID: "plv_1", WorkspaceID: "wsp_1", PlanID: "pln_1",
			Artifact: payload, CompilerVersion: artifact.CompilerVersion, Digest: "sha256:wrong",
		}, nil
	}})
	_, err = service.ResolveVersionForBinding(context.Background(), "wsp_1", "pln_1", "plv_1")
	require.Error(t, err)
	require.Contains(t, err.Error(), "digest metadata")
}

func TestServiceInspectVersionForBindingAllowsArchivedPlan(t *testing.T) {
	artifact, diagnostics, err := planlang.Compile([]byte(simplePlanSource))
	require.NoError(t, err)
	require.False(t, planlang.HasErrors(diagnostics), diagnostics)
	payload, err := artifact.DeterministicJSON()
	require.NoError(t, err)
	service := New(repositoryStub{inspectBindingVersion: func(_ context.Context, workspaceID string, planID string, versionID string) (plan.BindingVersionInspection, error) {
		return plan.BindingVersionInspection{
			PlanState: plan.PlanStateArchived,
			Version: plan.PlanVersion{
				ID: versionID, WorkspaceID: workspaceID, PlanID: planID,
				Artifact: payload, CompilerVersion: artifact.CompilerVersion, Digest: artifact.Digest,
			},
		}, nil
	}})
	inspection, err := service.InspectVersionForBinding(context.Background(), "wsp_1", "pln_1", "plv_1")
	require.NoError(t, err)
	require.Equal(t, PlanStateArchived, inspection.PlanState)
	require.Equal(t, "plv_1", inspection.Version.ID)
	require.Equal(t, artifact.Digest, inspection.Artifact.Digest)
}

const simplePlanSource = `apiVersion: loop.plan/v1
kind: PlanVersion
graph:
  entry: {nodeId: pause, port: in}
  nodes:
  - id: pause
    type: wait
    wait:
      until: {value: PT1M}
      timeout: PT2M
      retry: {maxAttempts: 2, backoff: 1s}
  - id: done
    type: end
    end: {outcome: succeeded}
  - id: failed
    type: end
    end: {outcome: failed, code: wait_failed}
  edges:
  - id: completed
    source: {nodeId: pause, port: completed}
    target: {nodeId: done, port: in}
  - id: timed_out
    source: {nodeId: pause, port: timed_out}
    target: {nodeId: failed, port: in}
  - id: action_failed
    source: {nodeId: pause, port: failed}
    target: {nodeId: failed, port: in}`

func int64Pointer(value int64) *int64    { return &value }
func stringPointer(value string) *string { return &value }
