package bindingmanage

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	bindingstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
	"github.com/tidbcloud/AgentTeam/internal/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/workflow/planmanage"
)

type fixedClock struct{ now time.Time }

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

const testScheduleSourceJSON = `{"type":"schedule","schedule":{"mode":"relative","relativeSeconds":60,"inputs":{"channel":"chn_1"}}}`

type agentOwnerVerifierStub struct {
	getAgent     func(context.Context, string, string, string) (agent.AgentView, error)
	getWorkspace func(context.Context, string, string) (workspace.Summary, error)
}

func (stub agentOwnerVerifierStub) GetAgentForUser(ctx context.Context, userID string, workspaceID string, agentID string) (agent.AgentView, error) {
	if stub.getAgent == nil {
		return agent.AgentView{}, errors.New("unexpected GetAgentForUser call")
	}
	return stub.getAgent(ctx, userID, workspaceID, agentID)
}

func (stub agentOwnerVerifierStub) GetWorkspaceForUser(ctx context.Context, userID string, workspaceID string) (workspace.Summary, error) {
	if stub.getWorkspace == nil {
		return workspace.Summary{}, errors.New("unexpected GetWorkspaceForUser call")
	}
	return stub.getWorkspace(ctx, userID, workspaceID)
}

type planResolverStub struct {
	resolve func(context.Context, string, string, string) (planmanage.ResolvedVersionForBinding, error)
	inspect func(context.Context, string, string, string) (planmanage.InspectedVersionForBinding, error)
}

func (stub planResolverStub) ResolveVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (planmanage.ResolvedVersionForBinding, error) {
	if stub.resolve == nil {
		return planmanage.ResolvedVersionForBinding{}, errors.New("unexpected ResolveVersionForBinding call")
	}
	return stub.resolve(ctx, workspaceID, planID, versionID)
}

func (stub planResolverStub) InspectVersionForBinding(ctx context.Context, workspaceID string, planID string, versionID string) (planmanage.InspectedVersionForBinding, error) {
	if stub.inspect == nil {
		return planmanage.InspectedVersionForBinding{}, errors.New("unexpected InspectVersionForBinding call")
	}
	return stub.inspect(ctx, workspaceID, planID, versionID)
}

type repositoryStub struct {
	create         func(context.Context, bindingstore.CreateBindingParams) (bindingstore.CreateResult, error)
	load           func(context.Context, string, string) (bindingstore.Binding, error)
	list           func(context.Context, bindingstore.ListBindingsParams) ([]bindingstore.Binding, error)
	update         func(context.Context, bindingstore.UpdateBindingMetadataParams) (bindingstore.Binding, error)
	createRevision func(context.Context, bindingstore.CreateRevisionParams) (bindingstore.CreateRevisionResult, error)
	loadRevision   func(context.Context, string, string, int64) (bindingstore.Revision, error)
	listRevisions  func(context.Context, bindingstore.ListRevisionsParams) ([]bindingstore.Revision, error)
	activate       func(context.Context, bindingstore.ActivateRevisionParams) (bindingstore.ActivateResult, error)
	disable        func(context.Context, bindingstore.DisableBindingParams) (bindingstore.Binding, error)
	archive        func(context.Context, bindingstore.ArchiveBindingParams) (bindingstore.Binding, error)
	restore        func(context.Context, bindingstore.RestoreBindingParams) (bindingstore.Binding, error)
}

type replayRepositoryStub struct {
	repositoryStub
	replay func(context.Context, bindingstore.ReplayCommandParams) (bindingstore.ReplayCommandResult, bool, error)
}

func (stub replayRepositoryStub) ReplayCommand(ctx context.Context, params bindingstore.ReplayCommandParams) (bindingstore.ReplayCommandResult, bool, error) {
	if stub.replay == nil {
		return bindingstore.ReplayCommandResult{}, false, errors.New("unexpected ReplayCommand call")
	}
	return stub.replay(ctx, params)
}

func (stub repositoryStub) CreateBinding(ctx context.Context, params bindingstore.CreateBindingParams) (bindingstore.CreateResult, error) {
	if stub.create == nil {
		return bindingstore.CreateResult{}, errors.New("unexpected CreateBinding call")
	}
	return stub.create(ctx, params)
}
func (stub repositoryStub) LoadBinding(ctx context.Context, workspaceID string, bindingID string) (bindingstore.Binding, error) {
	if stub.load == nil {
		return bindingstore.Binding{}, errors.New("unexpected LoadBinding call")
	}
	return stub.load(ctx, workspaceID, bindingID)
}
func (stub repositoryStub) ListBindings(ctx context.Context, params bindingstore.ListBindingsParams) ([]bindingstore.Binding, error) {
	if stub.list == nil {
		return nil, errors.New("unexpected ListBindings call")
	}
	return stub.list(ctx, params)
}
func (stub repositoryStub) UpdateBindingMetadata(ctx context.Context, params bindingstore.UpdateBindingMetadataParams) (bindingstore.Binding, error) {
	if stub.update == nil {
		return bindingstore.Binding{}, errors.New("unexpected UpdateBindingMetadata call")
	}
	return stub.update(ctx, params)
}
func (stub repositoryStub) CreateRevision(ctx context.Context, params bindingstore.CreateRevisionParams) (bindingstore.CreateRevisionResult, error) {
	if stub.createRevision == nil {
		return bindingstore.CreateRevisionResult{}, errors.New("unexpected CreateRevision call")
	}
	return stub.createRevision(ctx, params)
}
func (stub repositoryStub) LoadRevision(ctx context.Context, workspaceID string, bindingID string, revision int64) (bindingstore.Revision, error) {
	if stub.loadRevision == nil {
		return bindingstore.Revision{}, errors.New("unexpected LoadRevision call")
	}
	return stub.loadRevision(ctx, workspaceID, bindingID, revision)
}
func (stub repositoryStub) ListRevisions(ctx context.Context, params bindingstore.ListRevisionsParams) ([]bindingstore.Revision, error) {
	if stub.listRevisions == nil {
		return nil, errors.New("unexpected ListRevisions call")
	}
	return stub.listRevisions(ctx, params)
}
func (stub repositoryStub) ActivateRevision(ctx context.Context, params bindingstore.ActivateRevisionParams) (bindingstore.ActivateResult, error) {
	if stub.activate == nil {
		return bindingstore.ActivateResult{}, errors.New("unexpected ActivateRevision call")
	}
	return stub.activate(ctx, params)
}
func (stub repositoryStub) DisableBinding(ctx context.Context, params bindingstore.DisableBindingParams) (bindingstore.Binding, error) {
	if stub.disable == nil {
		return bindingstore.Binding{}, errors.New("unexpected DisableBinding call")
	}
	return stub.disable(ctx, params)
}
func (stub repositoryStub) ArchiveBinding(ctx context.Context, params bindingstore.ArchiveBindingParams) (bindingstore.Binding, error) {
	if stub.archive == nil {
		return bindingstore.Binding{}, errors.New("unexpected ArchiveBinding call")
	}
	return stub.archive(ctx, params)
}
func (stub repositoryStub) RestoreBinding(ctx context.Context, params bindingstore.RestoreBindingParams) (bindingstore.Binding, error) {
	if stub.restore == nil {
		return bindingstore.Binding{}, errors.New("unexpected RestoreBinding call")
	}
	return stub.restore(ctx, params)
}

func TestReplayCommandMapsAllAgentActionsWithoutResolvingPlanOrOwner(t *testing.T) {
	const requestDigest = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	actor := Actor{Type: ActorTypeAgent, ID: "agt_1", MemberID: "mem_agent_1"}

	for _, test := range []struct {
		action    string
		operation string
	}{
		{action: "create", operation: bindingstore.CommandOperationCreate},
		{action: "update", operation: bindingstore.CommandOperationMetadataUpdate},
		{action: "revise", operation: bindingstore.CommandOperationRevisionCreate},
		{action: "activate", operation: bindingstore.CommandOperationRevisionActivate},
		{action: "disable", operation: bindingstore.CommandOperationDisable},
		{action: "archive", operation: bindingstore.CommandOperationArchive},
		{action: "restore", operation: bindingstore.CommandOperationRestore},
	} {
		t.Run(test.action, func(t *testing.T) {
			called := false
			repository := replayRepositoryStub{replay: func(_ context.Context, params bindingstore.ReplayCommandParams) (bindingstore.ReplayCommandResult, bool, error) {
				called = true
				require.Equal(t, bindingstore.ReplayCommandParams{
					WorkspaceID: "wsp_1", Actor: bindingstore.Actor(actor), Operation: test.operation,
					IdempotencyKey: "command-1", RequestDigest: requestDigest,
				}, params)
				return bindingstore.ReplayCommandResult{}, false, nil
			}}
			service := New(repository, planResolverStub{})

			_, found, err := service.ReplayCommand(context.Background(), ReplayCommandInput{
				WorkspaceID: "wsp_1", Action: test.action, RequestDigest: requestDigest,
				Actor: actor, IdempotencyKey: "command-1",
			})
			require.NoError(t, err)
			require.False(t, found)
			require.True(t, called)
		})
	}
}

func TestReplayCommandRejectsHumanActorBeforeRepositoryLookup(t *testing.T) {
	called := false
	repository := replayRepositoryStub{replay: func(context.Context, bindingstore.ReplayCommandParams) (bindingstore.ReplayCommandResult, bool, error) {
		called = true
		return bindingstore.ReplayCommandResult{}, false, nil
	}}
	service := New(repository, planResolverStub{})

	_, found, err := service.ReplayCommand(context.Background(), ReplayCommandInput{
		WorkspaceID: "wsp_1", Action: "create",
		RequestDigest:  "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
		Actor:          Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"},
		IdempotencyKey: "command-1",
	})
	require.Error(t, err)
	require.False(t, found)
	require.False(t, called)
}

func TestCreateNormalizesExactVersionAndMaterializesRelativeAtActivation(t *testing.T) {
	now := time.Date(2026, 7, 21, 8, 0, 0, 0, time.UTC)
	owner := Owner{UserID: "usr_1", MemberID: "mem_1"}
	actor := Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}
	var captured bindingstore.CreateBindingParams
	repository := repositoryStub{create: func(_ context.Context, params bindingstore.CreateBindingParams) (bindingstore.CreateResult, error) {
		captured = params
		return storeCreateResult(params, now), nil
	}}
	resolver := planResolverStub{inspect: func(_ context.Context, workspaceID, planID, versionID string) (planmanage.InspectedVersionForBinding, error) {
		require.Equal(t, "wsp_1", workspaceID)
		require.Equal(t, "pln_1", planID)
		require.Equal(t, "plv_1", versionID)
		return inspectedPlanVersion(planmanage.PlanStateActive), nil
	}}
	service := New(repository, resolver, WithClock(fixedClock{now: now}))

	binding, revision, err := service.Create(context.Background(), CreateInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", Key: "daily_chain", DisplayName: " Daily chain ",
		PlanVersionID: "plv_1", Activate: true, Owner: owner, Actor: actor, IdempotencyKey: "call-1",
		Source: json.RawMessage(`{"type":"schedule","schedule":{"mode":"relative","relativeSeconds":90,"inputs":{"channel":"chn_1"}}}`),
	})
	require.NoError(t, err)
	require.Equal(t, "Daily chain", captured.DisplayName)
	require.Equal(t, "pln_1", captured.PlanID)
	require.Equal(t, "plv_1", captured.PlanVersionID)
	require.Equal(t, bindingstore.SourceKindSchedule, captured.SourceKind)
	require.NotNil(t, captured.NextOccurrenceAt)
	require.Equal(t, now.Add(90*time.Second), *captured.NextOccurrenceAt)
	require.Regexp(t, `^sha256:[0-9a-f]{64}$`, captured.Digest)
	require.Equal(t, "wbd_1", binding.ID)
	require.Equal(t, captured.Digest, revision.Digest)
	require.JSONEq(t, `{"type":"schedule","schedule":{"mode":"relative","relativeSeconds":90,"inputs":{"channel":"chn_1"}}}`, string(revision.Source))
}

func TestCreateInspectsArchivedPlanSoIdempotentReceiptRemainsReachable(t *testing.T) {
	now := time.Date(2026, 7, 21, 8, 0, 0, 0, time.UTC)
	called := false
	repository := repositoryStub{create: func(_ context.Context, params bindingstore.CreateBindingParams) (bindingstore.CreateResult, error) {
		// This stands in for the store's receipt-first branch. A genuinely new
		// write is still rejected by the store's in-transaction active fence.
		called = true
		return storeCreateResult(params, now), nil
	}}
	resolver := planResolverStub{inspect: func(context.Context, string, string, string) (planmanage.InspectedVersionForBinding, error) {
		return inspectedPlanVersion(planmanage.PlanStateArchived), nil
	}}
	service := New(repository, resolver, WithClock(fixedClock{now: now}))

	_, revision, err := service.Create(context.Background(), CreateInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", Key: "binding", DisplayName: "Binding",
		PlanVersionID: "plv_1", IdempotencyKey: "original-call",
		Owner:  Owner{UserID: "usr_1", MemberID: "mem_1"},
		Actor:  Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"},
		Source: json.RawMessage(testScheduleSourceJSON),
	})
	require.NoError(t, err)
	require.True(t, called)
	require.Equal(t, "plv_1", revision.PlanVersionID)
}

func TestCreateRejectsForgedHumanOwnerBeforePersistence(t *testing.T) {
	service := New(repositoryStub{}, planResolverStub{})
	_, _, err := service.Create(context.Background(), CreateInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", Key: "binding", DisplayName: "Binding",
		PlanVersionID: "plv_1", Source: json.RawMessage(`{}`),
		Owner: Owner{UserID: "usr_other", MemberID: "mem_other"},
		Actor: Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, IdempotencyKey: "call-1",
	})
	require.Error(t, err)
	require.ErrorIs(t, err, storeerr.ErrForbidden)
}

func TestEveryMutationRejectsForgedHumanAndUnverifiedAgentProvenance(t *testing.T) {
	type mutationCall func(*Service, Owner, Actor) error
	mutations := []struct {
		name string
		call mutationCall
	}{
		{name: "create", call: func(service *Service, principal Owner, actor Actor) error {
			_, _, err := service.Create(context.Background(), CreateInput{
				WorkspaceID: "wsp_1", PlanID: "pln_1", Key: "binding", DisplayName: "Binding",
				PlanVersionID: "plv_1", Source: json.RawMessage(testScheduleSourceJSON),
				Owner: principal, Actor: actor, IdempotencyKey: "call-create",
			})
			return err
		}},
		{name: "update", call: func(service *Service, principal Owner, actor Actor) error {
			displayName := "Updated"
			_, err := service.UpdateMetadata(context.Background(), UpdateMetadataInput{
				WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 1,
				DisplayName: &displayName, Principal: principal, Actor: actor, IdempotencyKey: "call-update",
			})
			return err
		}},
		{name: "create revision", call: func(service *Service, principal Owner, actor Actor) error {
			_, _, err := service.CreateRevision(context.Background(), CreateRevisionInput{
				WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 1,
				PlanVersionID: "plv_1", Source: json.RawMessage(testScheduleSourceJSON),
				Principal: principal, Actor: actor, IdempotencyKey: "call-revision",
			})
			return err
		}},
		{name: "activate", call: func(service *Service, principal Owner, actor Actor) error {
			_, _, err := service.Activate(context.Background(), ActivateInput{
				WorkspaceID: "wsp_1", BindingID: "wbd_1", Revision: 1, ExpectedRevision: 1,
				Principal: principal, Actor: actor, IdempotencyKey: "call-activate",
			})
			return err
		}},
		{name: "disable", call: func(service *Service, principal Owner, actor Actor) error {
			_, err := service.Disable(context.Background(), LifecycleInput{
				WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 1, Principal: principal, Actor: actor, IdempotencyKey: "call-disable",
			})
			return err
		}},
		{name: "archive", call: func(service *Service, principal Owner, actor Actor) error {
			_, err := service.Archive(context.Background(), LifecycleInput{
				WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 1, Principal: principal, Actor: actor, IdempotencyKey: "call-archive",
			})
			return err
		}},
		{name: "restore", call: func(service *Service, principal Owner, actor Actor) error {
			_, err := service.Restore(context.Background(), LifecycleInput{
				WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 1, Principal: principal, Actor: actor, IdempotencyKey: "call-restore",
			})
			return err
		}},
	}
	principal := Owner{UserID: "usr_1", MemberID: "mem_1"}
	identities := []struct {
		name  string
		actor Actor
	}{
		{name: "forged human", actor: Actor{Type: ActorTypeHuman, ID: "usr_attacker", MemberID: "mem_attacker"}},
		{name: "unverified agent", actor: Actor{Type: ActorTypeAgent, ID: "agt_attacker", MemberID: "mem_agent_attacker"}},
	}
	for _, mutation := range mutations {
		for _, identity := range identities {
			t.Run(mutation.name+"/"+identity.name, func(t *testing.T) {
				service := New(repositoryStub{}, planResolverStub{})
				err := mutation.call(service, principal, identity.actor)
				require.Error(t, err)
				require.ErrorIs(t, err, storeerr.ErrForbidden)
				code, _, ok := ErrorDetails(err)
				require.True(t, ok)
				require.Equal(t, ErrorCodeForbidden, code)
			})
		}
	}
}

func TestAgentOwnerVerifierMustMatchActorAndPrincipalExactly(t *testing.T) {
	principal := Owner{UserID: "usr_1", MemberID: "mem_1"}
	actor := Actor{Type: ActorTypeAgent, ID: "agt_1", MemberID: "mem_agent"}
	validAgent := agent.AgentView{AgentID: actor.ID, MemberID: actor.MemberID}
	validWorkspace := workspace.Summary{WorkspaceID: "wsp_1", MemberUserID: principal.UserID, MemberID: principal.MemberID}
	tests := []struct {
		name      string
		agent     agent.AgentView
		workspace workspace.Summary
	}{
		{name: "agent id", agent: agent.AgentView{AgentID: "agt_other", MemberID: actor.MemberID}, workspace: validWorkspace},
		{name: "agent member", agent: agent.AgentView{AgentID: actor.ID, MemberID: "mem_other"}, workspace: validWorkspace},
		{name: "workspace", agent: validAgent, workspace: workspace.Summary{WorkspaceID: "wsp_other", MemberUserID: principal.UserID, MemberID: principal.MemberID}},
		{name: "owner user", agent: validAgent, workspace: workspace.Summary{WorkspaceID: "wsp_1", MemberUserID: "usr_other", MemberID: principal.MemberID}},
		{name: "owner member", agent: validAgent, workspace: workspace.Summary{WorkspaceID: "wsp_1", MemberUserID: principal.UserID, MemberID: "mem_other"}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			service := &Service{agentOwners: agentOwnerVerifierStub{
				getAgent: func(context.Context, string, string, string) (agent.AgentView, error) { return test.agent, nil },
				getWorkspace: func(context.Context, string, string) (workspace.Summary, error) {
					return test.workspace, nil
				},
			}}
			_, _, err := service.normalizeMutationIdentity(context.Background(), "wsp_1", principal, actor)
			require.ErrorIs(t, err, storeerr.ErrForbidden)
		})
	}

	service := &Service{agentOwners: boundAgentOwnerVerifier(principal, actor)}
	normalizedPrincipal, normalizedActor, err := service.normalizeMutationIdentity(context.Background(), "wsp_1", principal, actor)
	require.NoError(t, err)
	require.Equal(t, principal, normalizedPrincipal)
	require.Equal(t, actor, normalizedActor)
}

func TestValidateRejectsRemovedManualSourceWithoutPersistence(t *testing.T) {
	service := New(repositoryStub{}, planResolverStub{resolve: func(context.Context, string, string, string) (planmanage.ResolvedVersionForBinding, error) {
		return resolvedPlanVersion(), nil
	}})
	validation, err := service.Validate(context.Background(), ValidateInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Source: json.RawMessage(`{"type":"manual"}`),
	})
	require.NoError(t, err)
	require.Empty(t, validation.Digest)
	require.Len(t, validation.Diagnostics, 1)
	require.Equal(t, "binding.source_type", validation.Diagnostics[0].Code)
}

func TestValidateUsesActiveOnlyPlanResolution(t *testing.T) {
	resolveCalled := false
	service := New(repositoryStub{}, planResolverStub{
		resolve: func(context.Context, string, string, string) (planmanage.ResolvedVersionForBinding, error) {
			resolveCalled = true
			return planmanage.ResolvedVersionForBinding{}, planmanage.RequestError{
				Kind: storeerr.ErrConflict, Code: planmanage.ErrorCodeArchived, Message: "Plan is archived",
			}
		},
		inspect: func(context.Context, string, string, string) (planmanage.InspectedVersionForBinding, error) {
			return inspectedPlanVersion(planmanage.PlanStateArchived), nil
		},
	})
	_, err := service.Validate(context.Background(), ValidateInput{
		WorkspaceID: "wsp_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Source: json.RawMessage(testScheduleSourceJSON),
	})
	require.Error(t, err)
	require.True(t, resolveCalled)
	code, _, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodePlanArchived, code)
}

func TestCreateRevisionInspectsArchivedPlanSoIdempotentReceiptRemainsReachable(t *testing.T) {
	stable := storeBinding(BindingStateDisabled, 3)
	called := false
	repository := repositoryStub{
		load: func(context.Context, string, string) (bindingstore.Binding, error) { return stable, nil },
		createRevision: func(_ context.Context, params bindingstore.CreateRevisionParams) (bindingstore.CreateRevisionResult, error) {
			called = true
			return bindingstore.CreateRevisionResult{
				Binding: stable,
				Revision: bindingstore.Revision{
					ID: "wbr_2", WorkspaceID: params.WorkspaceID, BindingID: params.BindingID, PlanID: stable.PlanID,
					Revision: 2, PlanVersionID: params.PlanVersionID, SourceKind: params.SourceKind,
					ConfigJSON: params.ConfigJSON, Digest: params.Digest, CreatedBy: params.Actor,
				},
				Replayed: true,
			}, nil
		},
	}
	resolver := planResolverStub{inspect: func(context.Context, string, string, string) (planmanage.InspectedVersionForBinding, error) {
		return inspectedPlanVersion(planmanage.PlanStateArchived), nil
	}}
	service := New(repository, resolver)

	_, revision, err := service.CreateRevision(context.Background(), CreateRevisionInput{
		WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 3,
		PlanVersionID: "plv_1", IdempotencyKey: "original-revision-call",
		Principal: Owner{UserID: "usr_1", MemberID: "mem_1"},
		Actor:     Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"},
		Source:    json.RawMessage(testScheduleSourceJSON),
	})
	require.NoError(t, err)
	require.True(t, called)
	require.Equal(t, int64(2), revision.Revision)
}

func TestCreateRevisionRejectsNonOwnerBeforePlanResolution(t *testing.T) {
	stable := storeBinding(BindingStateDisabled, 3)
	service := New(repositoryStub{load: func(context.Context, string, string) (bindingstore.Binding, error) {
		return stable, nil
	}}, planResolverStub{})
	_, _, err := service.CreateRevision(context.Background(), CreateRevisionInput{
		WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 3, PlanVersionID: "plv_1",
		Source: json.RawMessage(`{}`), Principal: Owner{UserID: "usr_2", MemberID: "mem_2"},
		Actor: Actor{Type: ActorTypeHuman, ID: "usr_2", MemberID: "mem_2"}, IdempotencyKey: "call-2",
	})
	require.Error(t, err)
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	code, _, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeForbidden, code)
}

func TestActivateUsesStoredRevisionAndOwnerFence(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 0, 0, 0, time.UTC)
	stable := storeBinding(BindingStateDisabled, 3)
	principal := Owner{UserID: "usr_1", MemberID: "mem_1"}
	actor := Actor{Type: ActorTypeAgent, ID: "agt_1", MemberID: "mem_agent"}
	storedRevision := normalizedStoreRevision(t, binding.RevisionSpec{
		PlanID: "pln_1", PlanVersionID: "plv_1",
		InputPolicy: binding.InputPolicy{Defaults: json.RawMessage(`{}`), Constants: json.RawMessage(`{}`)},
		Source: binding.Source{Type: binding.SourceTypeSchedule, Schedule: &binding.ScheduleSource{
			Mode: binding.ScheduleModeRelative, RelativeSeconds: int64Pointer(30), Inputs: json.RawMessage(`{"channel":"chn_1"}`),
		}},
	})
	var captured bindingstore.ActivateRevisionParams
	repository := repositoryStub{
		load: func(context.Context, string, string) (bindingstore.Binding, error) { return stable, nil },
		loadRevision: func(context.Context, string, string, int64) (bindingstore.Revision, error) {
			return storedRevision, nil
		},
		activate: func(_ context.Context, params bindingstore.ActivateRevisionParams) (bindingstore.ActivateResult, error) {
			captured = params
			stable.State = bindingstore.BindingStateEnabled
			stable.Revision++
			return bindingstore.ActivateResult{Binding: stable, Revision: storedRevision}, nil
		},
	}
	service := New(
		repository,
		planResolverStub{},
		WithClock(fixedClock{now: now}),
		WithAgentOwnerVerifier(boundAgentOwnerVerifier(principal, actor)),
	)
	binding, revision, err := service.Activate(context.Background(), ActivateInput{
		WorkspaceID: "wsp_1", BindingID: "wbd_1", Revision: 1, ExpectedRevision: 3,
		Principal: principal,
		Actor:     actor, IdempotencyKey: "mcp-call-1",
	})
	require.NoError(t, err)
	require.NotNil(t, captured.NextOccurrenceAt)
	require.Equal(t, now.Add(30*time.Second), *captured.NextOccurrenceAt)
	require.Equal(t, BindingStateEnabled, binding.State)
	require.Equal(t, int64(1), revision.Revision)
}

func TestListRevisionsChecksBindingExistence(t *testing.T) {
	service := New(repositoryStub{load: func(context.Context, string, string) (bindingstore.Binding, error) {
		return bindingstore.Binding{}, storeerr.NotFound("missing")
	}}, planResolverStub{})
	_, err := service.ListRevisions(context.Background(), ListRevisionsInput{WorkspaceID: "wsp_1", BindingID: "wbd_missing"})
	require.Error(t, err)
	require.ErrorIs(t, err, storeerr.ErrNotFound)
}

func TestArchiveUsesTrustedOwnerAndMapsRevisionConflict(t *testing.T) {
	stable := storeBinding(BindingStateEnabled, 4)
	repository := repositoryStub{
		load: func(context.Context, string, string) (bindingstore.Binding, error) { return stable, nil },
		archive: func(context.Context, bindingstore.ArchiveBindingParams) (bindingstore.Binding, error) {
			return bindingstore.Binding{}, bindingstore.RevisionConflictError{Resource: "binding", Expected: 4, Actual: 5}
		},
	}
	service := New(repository, planResolverStub{})
	_, err := service.Archive(context.Background(), LifecycleInput{
		WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 4,
		Principal: Owner{UserID: "usr_1", MemberID: "mem_1"},
		Actor:     Actor{Type: ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"}, IdempotencyKey: "archive-call",
	})
	require.Error(t, err)
	code, details, ok := ErrorDetails(err)
	require.True(t, ok)
	require.Equal(t, ErrorCodeRevisionConflict, code)
	require.Equal(t, int64(5), details.(RevisionConflictDetails).CurrentRevision)
}

func TestMetadataAndLifecycleForwardDurableIdempotencyKeys(t *testing.T) {
	stable := storeBinding(BindingStateEnabled, 4)
	var updateKey string
	var archiveKey string
	repository := repositoryStub{
		load: func(context.Context, string, string) (bindingstore.Binding, error) { return stable, nil },
		update: func(_ context.Context, params bindingstore.UpdateBindingMetadataParams) (bindingstore.Binding, error) {
			updateKey = params.IdempotencyKey
			updated := stable
			updated.Revision++
			return updated, nil
		},
		archive: func(_ context.Context, params bindingstore.ArchiveBindingParams) (bindingstore.Binding, error) {
			archiveKey = params.IdempotencyKey
			archived := stable
			archived.State = bindingstore.BindingStateArchived
			archived.Revision++
			return archived, nil
		},
	}
	service := New(repository, planResolverStub{})
	principal := Owner{UserID: "usr_1", MemberID: "mem_1"}
	actor := Actor{Type: ActorTypeHuman, ID: principal.UserID, MemberID: principal.MemberID}

	name := "Renamed"
	_, err := service.UpdateMetadata(context.Background(), UpdateMetadataInput{
		WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 4,
		DisplayName: &name, Principal: principal, Actor: actor, IdempotencyKey: " update-command ",
	})
	require.NoError(t, err)
	require.Equal(t, "update-command", updateKey)

	_, err = service.Archive(context.Background(), LifecycleInput{
		WorkspaceID: "wsp_1", BindingID: "wbd_1", ExpectedRevision: 4,
		Principal: principal, Actor: actor, IdempotencyKey: " archive-command ",
	})
	require.NoError(t, err)
	require.Equal(t, "archive-command", archiveKey)
}

func resolvedPlanVersion() planmanage.ResolvedVersionForBinding {
	return planmanage.ResolvedVersionForBinding{
		Version:  planmanage.Version{ID: "plv_1", WorkspaceID: "wsp_1", PlanID: "pln_1"},
		Artifact: &plan.Artifact{Contracts: bindingPlanContract()},
	}
}

func inspectedPlanVersion(state planmanage.PlanState) planmanage.InspectedVersionForBinding {
	resolved := resolvedPlanVersion()
	return planmanage.InspectedVersionForBinding{
		PlanState: state,
		Version:   resolved.Version,
		Artifact:  resolved.Artifact,
	}
}

func boundAgentOwnerVerifier(owner Owner, actor Actor) agentOwnerVerifierStub {
	return agentOwnerVerifierStub{
		getAgent: func(_ context.Context, userID string, workspaceID string, agentID string) (agent.AgentView, error) {
			if userID != owner.UserID || workspaceID != "wsp_1" || agentID != actor.ID {
				return agent.AgentView{}, storeerr.ErrForbidden
			}
			return agent.AgentView{AgentID: actor.ID, MemberID: actor.MemberID}, nil
		},
		getWorkspace: func(_ context.Context, userID string, workspaceID string) (workspace.Summary, error) {
			if userID != owner.UserID || workspaceID != "wsp_1" {
				return workspace.Summary{}, storeerr.ErrForbidden
			}
			return workspace.Summary{WorkspaceID: workspaceID, MemberUserID: owner.UserID, MemberID: owner.MemberID}, nil
		},
	}
}

func bindingPlanContract() plan.ArtifactContracts {
	return plan.ArtifactContracts{Input: plan.ArtifactContract{
		{Name: "channel", Schema: plan.ArtifactSchema{Type: plan.TypeChannel, Required: true, Values: []string{}}},
	}}
}

func storeCreateResult(params bindingstore.CreateBindingParams, now time.Time) bindingstore.CreateResult {
	binding := storeBinding(BindingStateDisabled, 1)
	if params.Activate {
		binding.State = bindingstore.BindingStateEnabled
		binding.ActiveRevision = &bindingstore.RevisionRef{ID: "wbr_1", Revision: 1}
		binding.ActiveSourceID = "wbs_1"
	}
	binding.Key, binding.DisplayName, binding.DescriptionMD = params.Key, params.DisplayName, params.DescriptionMD
	binding.Owner, binding.CreatedBy, binding.UpdatedBy = params.Owner, params.Actor, params.Actor
	binding.CreatedAt, binding.UpdatedAt = now, now
	revision := bindingstore.Revision{
		ID: "wbr_1", WorkspaceID: params.WorkspaceID, BindingID: "wbd_1", PlanID: params.PlanID,
		Revision: 1, PlanVersionID: params.PlanVersionID, SourceKind: params.SourceKind,
		ConfigJSON: params.ConfigJSON, Digest: params.Digest, CreatedBy: params.Actor, CreatedAt: now,
	}
	return bindingstore.CreateResult{Binding: binding, Revision: revision}
}

func storeBinding(state BindingState, revision int64) bindingstore.Binding {
	return bindingstore.Binding{
		ID: "wbd_1", WorkspaceID: "wsp_1", PlanID: "pln_1", Key: "binding", DisplayName: "Binding",
		State: string(state), Revision: revision,
		LatestRevision: bindingstore.RevisionRef{ID: "wbr_1", Revision: 1},
		Owner:          OwnerToStore(Owner{UserID: "usr_1", MemberID: "mem_1"}),
		CreatedBy:      bindingstore.Actor{Type: bindingstore.ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"},
		UpdatedBy:      bindingstore.Actor{Type: bindingstore.ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"},
	}
}

func OwnerToStore(owner Owner) bindingstore.Owner {
	return bindingstore.Owner{UserID: owner.UserID, MemberID: owner.MemberID}
}

func normalizedStoreRevision(t *testing.T, spec binding.RevisionSpec) bindingstore.Revision {
	t.Helper()
	normalized, err := binding.NormalizeRevision(spec, bindingPlanContract().Input)
	require.NoError(t, err)
	return bindingstore.Revision{
		ID: "wbr_1", WorkspaceID: "wsp_1", BindingID: "wbd_1", PlanID: "pln_1", Revision: 1,
		PlanVersionID: "plv_1", SourceKind: string(normalized.Spec.Source.Type),
		ConfigJSON: normalized.ConfigJSON, Digest: normalized.Digest,
		CreatedBy: bindingstore.Actor{Type: bindingstore.ActorTypeHuman, ID: "usr_1", MemberID: "mem_1"},
	}
}

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