package orchestrator

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

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

type approvalDecisionRepositoryStub struct {
	run        run.Run
	activation run.Activation
	effect     run.Effect
	resolve    func(run.ResolveAwaitingEffectParams) (run.ResolveAwaitingEffectResult, error)
}

func (stub *approvalDecisionRepositoryStub) LoadEffectByID(context.Context, string, string) (run.Effect, error) {
	if stub.effect.ID == "" {
		return run.Effect{}, storeerr.NotFound("missing")
	}
	return stub.effect, nil
}

func (stub *approvalDecisionRepositoryStub) LoadRun(context.Context, string, string) (run.Run, error) {
	return stub.run, nil
}

func (stub *approvalDecisionRepositoryStub) LoadActivation(context.Context, string, string, string) (run.Activation, error) {
	return stub.activation, nil
}

func (stub *approvalDecisionRepositoryStub) ResolveAwaitingEffect(_ context.Context, params run.ResolveAwaitingEffectParams) (run.ResolveAwaitingEffectResult, error) {
	if stub.resolve == nil {
		return run.ResolveAwaitingEffectResult{}, errors.New("unexpected resolve")
	}
	return stub.resolve(params)
}

func TestApprovalDecisionServiceCommitsFirstDecisionAndPreservesReceipt(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 0, 0, 123456000, time.UTC)
	repository := approvalDecisionFixture(t, now)
	var captured run.ResolveAwaitingEffectParams
	repository.resolve = func(params run.ResolveAwaitingEffectParams) (run.ResolveAwaitingEffectResult, error) {
		captured = params
		effect := repository.effect
		effect.Status = run.EffectStatusCompleted
		return run.ResolveAwaitingEffectResult{Run: repository.run, Activation: repository.activation, Effect: effect}, nil
	}
	service, err := NewApprovalDecisionService(repository)
	require.NoError(t, err)
	decision := ApprovalDecision{
		WorkspaceID: "wsp_1", EffectID: "wfx_approve", NotificationID: "btn_1",
		ActorMemberID: "mem_alice", Decision: ApprovalDecisionApproved,
		ClientDecisionID: "action_1", DecidedAt: now,
	}
	result, err := service.DecideApproval(context.Background(), decision)
	require.NoError(t, err)
	require.Equal(t, ApprovalDecisionApproved, result.Decision)
	require.Equal(t, "mem_alice", result.DecidedByMemberID)
	require.Equal(t, "Approve deployment", result.Summary)
	require.Equal(t, "a000001", result.ActivationID)
	require.False(t, result.Replayed)
	require.Equal(t, ApprovalDecisionApproved, captured.SelectedPort)
	require.JSONEq(t, `{}`, string(captured.ActivationOutputJSON))
	terminal, err := DecodeEffectResult(captured.EffectResultJSON)
	require.NoError(t, err)
	require.Equal(t, "btn_1", terminal.NotificationID)
	require.Equal(t, "msg_card", terminal.MessageID)
	require.Equal(t, "mem_alice", terminal.AssigneeMemberID)
	require.Equal(t, "mem_alice", terminal.DecidedByMemberID)
	require.Equal(t, "action_1", terminal.IdempotencyKey)
	require.Equal(t, ApprovalDecisionApproved, terminal.Port)
	require.Equal(t, now, *terminal.CompletedAt)
	require.Len(t, captured.Events, 1)
	require.Equal(t, "approval_decided", captured.Events[0].Type)
}

func TestApprovalDecisionServicePassesFirstDecisionPreCommitGuard(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 0, 0, 0, time.UTC)
	repository := approvalDecisionFixture(t, now)
	repository.resolve = func(params run.ResolveAwaitingEffectParams) (run.ResolveAwaitingEffectResult, error) {
		require.NotNil(t, params.PreCommitGuard)
		effect := repository.effect
		effect.Status = run.EffectStatusCompleted
		return run.ResolveAwaitingEffectResult{Run: repository.run, Activation: repository.activation, Effect: effect}, nil
	}
	service, err := NewApprovalDecisionService(repository)
	require.NoError(t, err)
	called := false
	_, err = service.DecideApprovalWithPreCommit(context.Background(), ApprovalDecision{
		WorkspaceID: "wsp_1", EffectID: "wfx_approve", NotificationID: "btn_1",
		ActorMemberID: "mem_alice", Decision: ApprovalDecisionApproved,
		ClientDecisionID: "action_1", DecidedAt: now,
	}, func(context.Context, coredb.Tx, run.Run, run.Activation, run.Effect) error {
		called = true
		return nil
	})
	require.NoError(t, err)
	require.False(t, called, "repository owns invoking the guard at its transaction boundary")
}

func TestApprovalDecisionServiceEnforcesFrozenAssigneeAndNotification(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 0, 0, 0, time.UTC)
	for _, testCase := range []struct {
		name     string
		mutate   func(*approvalDecisionRepositoryStub, *ApprovalDecision)
		expected error
	}{
		{name: "actor", mutate: func(_ *approvalDecisionRepositoryStub, command *ApprovalDecision) { command.ActorMemberID = "mem_eve" }, expected: ErrApprovalUnauthorized},
		{name: "notification", mutate: func(_ *approvalDecisionRepositoryStub, command *ApprovalDecision) {
			command.NotificationID = "btn_other"
		}, expected: ErrApprovalUnauthorized},
		{name: "terminal", mutate: func(repository *approvalDecisionRepositoryStub, _ *ApprovalDecision) {
			repository.run.Status = run.RunStatusCanceled
		}, expected: ErrApprovalNotPending},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			repository := approvalDecisionFixture(t, now)
			command := ApprovalDecision{
				WorkspaceID: "wsp_1", EffectID: "wfx_approve", NotificationID: "btn_1",
				ActorMemberID: "mem_alice", Decision: ApprovalDecisionRejected,
				ClientDecisionID: "action_1", DecidedAt: now,
			}
			testCase.mutate(repository, &command)
			service, err := NewApprovalDecisionService(repository)
			require.NoError(t, err)
			_, err = service.DecideApproval(context.Background(), command)
			require.ErrorIs(t, err, testCase.expected)
		})
	}
}

func TestApprovalDecisionServiceLoadsCompletedReceiptForCardConvergence(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 0, 0, 0, time.UTC)
	repository := approvalDecisionFixture(t, now)
	repository.effect.Status = run.EffectStatusCompleted
	repository.effect.CompletedAt = &now
	receipt, err := DecodeEffectResult(repository.effect.ResultJSON)
	require.NoError(t, err)
	receipt.Port = ApprovalDecisionApproved
	receipt.DecidedByMemberID = "mem_alice"
	receipt.IdempotencyKey = "action_1"
	receipt.ApprovalComment = "durable card retry"
	receipt.ApprovalFeedbackMessageID = "msg_feedback"
	receipt.ApprovalFeedbackClientMessageID = "workflow-approval-feedback:wfx_approve:approved"
	receipt.CompletedAt = &now
	repository.effect.ResultJSON, err = EncodeEffectResult(receipt)
	require.NoError(t, err)
	service, err := NewApprovalDecisionService(repository)
	require.NoError(t, err)

	result, err := service.LoadCompletedApproval(context.Background(), "wsp_1", "wfx_approve", "btn_1", "mem_alice")
	require.NoError(t, err)
	require.True(t, result.Replayed)
	require.Equal(t, ApprovalDecisionApproved, result.Decision)
	require.Equal(t, "durable card retry", result.Comment)
	require.Equal(t, "msg_feedback", result.FeedbackMessageID)
	require.Equal(t, "workflow-approval-feedback:wfx_approve:approved", result.FeedbackClientMessageID)

	_, err = service.LoadCompletedApproval(context.Background(), "wsp_1", "wfx_approve", "btn_1", "mem_eve")
	require.ErrorIs(t, err, ErrApprovalUnauthorized)
}

func TestApprovalDecisionServiceSameActionReplaysAndConflictsFenceLosers(t *testing.T) {
	now := time.Date(2026, 7, 21, 9, 0, 0, 0, time.UTC)
	repository := approvalDecisionFixture(t, now)
	repository.effect.Status = run.EffectStatusCompleted
	repository.activation.Status = run.ActivationStatusSucceeded
	receipt, err := DecodeEffectResult(repository.effect.ResultJSON)
	require.NoError(t, err)
	receipt.Port = ApprovalDecisionApproved
	receipt.Output = runkernel.Values{}
	receipt.DecidedByMemberID = "mem_alice"
	receipt.IdempotencyKey = "action_1"
	receipt.CompletedAt = &now
	repository.effect.ResultJSON, err = EncodeEffectResult(receipt)
	require.NoError(t, err)
	service, err := NewApprovalDecisionService(repository)
	require.NoError(t, err)

	result, err := service.DecideApproval(context.Background(), ApprovalDecision{
		WorkspaceID: "wsp_1", EffectID: "wfx_approve", NotificationID: "btn_1",
		ActorMemberID: "mem_alice", Decision: ApprovalDecisionApproved,
		ClientDecisionID: "action_1", DecidedAt: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.True(t, result.Replayed)
	require.Equal(t, now, result.DecidedAt)

	_, err = service.DecideApproval(context.Background(), ApprovalDecision{
		WorkspaceID: "wsp_1", EffectID: "wfx_approve", NotificationID: "btn_1",
		ActorMemberID: "mem_alice", Decision: ApprovalDecisionRejected,
		ClientDecisionID: "action_2", DecidedAt: now.Add(time.Minute),
	})
	require.ErrorIs(t, err, ErrApprovalDecisionConflict)
}

func approvalDecisionFixture(t *testing.T, now time.Time) *approvalDecisionRepositoryStub {
	t.Helper()
	request, err := EncodeEffectRequest(EffectRequest{Intent: runkernel.Intent{
		Kind: runkernel.IntentApproval, ActivationID: "a000001", NodeID: "approve",
		Assignee: "@alice", Summary: "Approve deployment",
		Retry: plan.ArtifactRetryPolicy{MaxAttempts: 2, BackoffNanos: int64(time.Second)},
	}})
	require.NoError(t, err)
	receipt, err := EncodeEffectResult(EffectResult{
		NotificationID: "btn_1", NotificationRevision: 1, MessageID: "msg_card", AssigneeMemberID: "mem_alice",
	})
	require.NoError(t, err)
	return &approvalDecisionRepositoryStub{
		run: run.Run{
			ID: "wri_1", WorkspaceID: "wsp_1", PlanID: "wfp_1", PlanVersionID: "wfv_1",
			Status: run.RunStatusWaiting,
		},
		activation: run.Activation{
			ID: "wra_1", WorkspaceID: "wsp_1", RunID: "wri_1", LogicalKey: "a000001", NodeID: "approve",
			Status: run.ActivationStatusWaiting, InputJSON: json.RawMessage(`{}`),
		},
		effect: run.Effect{
			ID: "wfx_approve", WorkspaceID: "wsp_1", RunID: "wri_1", ActivationID: "wra_1",
			ApprovalAssigneeMemberID: "mem_alice", Kind: EffectKindApproval,
			Status: run.EffectStatusAwaiting, RequestJSON: request, ResultJSON: receipt,
		},
	}
}
