package execution

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

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	executionstore "github.com/tidbcloud/AgentTeam/internal/store/role/execution"
)

func TestExecuteFailsInvalidApprovalFactsWithSafeProjection(t *testing.T) {
	for _, testCase := range []struct {
		name   string
		mutate func(*approval.Instance)
	}{
		{"id", func(value *approval.Instance) { value.ID = "appr_other" }},
		{"id empty", func(value *approval.Instance) { value.ID = "" }},
		{"id blank", func(value *approval.Instance) { value.ID = " " }},
		{"id untrimmed", func(value *approval.Instance) { value.ID = " appr_1 " }},
		{"workspace", func(value *approval.Instance) { value.WorkspaceID = "ws_other" }},
		{"state", func(value *approval.Instance) { value.State = approval.StatePending }},
		{"decision", func(value *approval.Instance) { value.Decision = approval.DecisionRejected }},
		{"version zero", func(value *approval.Instance) { value.Version = 0 }},
		{"version one", func(value *approval.Instance) { value.Version = 1 }},
		{"decider", func(value *approval.Instance) { value.DecidedByMemberID = "" }},
		{"decider blank", func(value *approval.Instance) { value.DecidedByMemberID = " " }},
		{"decider untrimmed", func(value *approval.Instance) { value.DecidedByMemberID = " mem_manager " }},
		{"applicant member", func(value *approval.Instance) { value.ApplicantMemberID = "" }},
		{"applicant member blank", func(value *approval.Instance) { value.ApplicantMemberID = " " }},
		{"applicant member untrimmed", func(value *approval.Instance) { value.ApplicantMemberID = " mem_1 " }},
		{"subject type", func(value *approval.Instance) { value.SubjectType = "other" }},
		{"subject version", func(value *approval.Instance) { value.SubjectVersion = 2 }},
		{"subject ref", func(value *approval.Instance) { value.SubjectRef = "wrong" }},
		{"idempotency", func(value *approval.Instance) { value.IdempotencyKey = "wrong" }},
		{"subject and idempotency same wrong", func(value *approval.Instance) {
			value.SubjectRef, value.IdempotencyKey = "same_wrong", "same_wrong"
		}},
		{"digest", func(value *approval.Instance) { value.PayloadDigest = strings.Repeat("0", 64) }},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			service, probe, claim, claimNow := newExecutionProbe(
				t, RoleCreateProposal{Name: "Release", Definition: "Review releases"},
			)
			projectCalls := 0
			service.dependencies.ProjectTerminal = func(
				context.Context, approval.Instance, RequestResult,
			) error {
				projectCalls++
				return nil
			}
			testCase.mutate(&probe.approval)

			err := service.Execute(context.Background(), claim, claimNow)
			require.NoError(t, err)
			require.Equal(t, []string{"load", "now", "fail"}, probe.calls)
			require.Zero(t, probe.projectedLoads)
			require.Zero(t, projectCalls)
			requireSafeFailure(t, probe.settleFailures[0], "invalid_request")
			requireSingleSettle(t, probe, claim, claimNow, "fail")
		})
	}
}

func TestExecuteProjectsTerminalBeforeSettleAndRetriesSameReceipt(t *testing.T) {
	testExecuteTerminalProjectionRetry(t, "success projection")
}

func TestExecuteProjectsSafeDeterministicFailureBeforeSettle(t *testing.T) {
	testExecuteTerminalProjectionRetry(t, "failure projection")
}

func TestExecuteRetriesSettleAfterMatchingTerminalCardWithoutRemutation(t *testing.T) {
	testExecuteTerminalProjectionRetry(t, "settle")
}

func TestExecuteRetriesResolvedSuccessAfterAuthorityDrift(t *testing.T) {
	testExecuteResolvedTerminalAfterDependencyDrift(t, RequestSucceeded)
}

func TestExecuteRetriesResolvedFailureAfterAuthorityRecovers(t *testing.T) {
	testExecuteResolvedTerminalAfterDependencyDrift(t, RequestFailed)
}

func TestExecuteProjectedTerminalLoadErrorHasNoExecutionEffects(t *testing.T) {
	service, probe, claim, claimNow := newExecutionProbe(
		t, RoleCreateProposal{Name: "Release", Definition: "Review releases"},
	)
	injected := errors.New("load projected terminal")
	probe.projectedErr = injected
	projectCalls := 0
	service.dependencies.ProjectTerminal = func(
		context.Context, approval.Instance, RequestResult,
	) error {
		projectCalls++
		return nil
	}

	require.ErrorIs(t, service.Execute(context.Background(), claim, claimNow), injected)
	require.Equal(t, 1, probe.projectedLoads)
	require.Equal(t, []string{"load"}, probe.calls)
	require.Zero(t, projectCalls)
	require.Zero(t, totalSettles(probe))
}

func TestExecuteRejectsInvalidProjectedTerminalWithoutEffects(t *testing.T) {
	for _, testCase := range []struct {
		name   string
		mutate func(*RequestResult)
	}{
		{"processing", func(result *RequestResult) { result.Status = RequestProcessing }},
		{"identity mismatch", func(result *RequestResult) { result.RequestID = "req_other" }},
		{"unsafe failure", func(result *RequestResult) {
			result.Status = RequestFailed
			result.RoleID, result.RoleVersionID = "", ""
			result.FailureCode = string(ApplyNotFound)
			result.FailureMessage = "raw upstream error"
		}},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			service, probe, claim, claimNow := newExecutionProbe(
				t, RoleCreateProposal{Name: "Release", Definition: "Review releases"},
			)
			probe.projected = RequestResult{
				ApprovalID: "appr_1", ApplicantAgentID: "agt_1", RequestID: "req_1",
				Action: ActionCreate, Status: RequestSucceeded,
				RoleID: "role_1", RoleVersionID: "rlv_1",
			}
			testCase.mutate(&probe.projected)
			probe.projectedOK = true
			projectCalls := 0
			service.dependencies.ProjectTerminal = func(
				context.Context, approval.Instance, RequestResult,
			) error {
				projectCalls++
				return nil
			}

			require.ErrorIs(t, service.Execute(context.Background(), claim, claimNow), ErrInvariant)
			require.Equal(t, 1, probe.projectedLoads)
			require.Equal(t, []string{"load"}, probe.calls)
			require.Zero(t, projectCalls)
			require.Zero(t, totalSettles(probe))
		})
	}
}

func testExecuteResolvedTerminalAfterDependencyDrift(
	t *testing.T,
	terminalStatus RequestStatus,
) {
	t.Helper()
	proposal := RoleUpdateProposal{
		RoleID: "role_1", ExpectedRoleVersionID: "rlv_expected",
		Name: ptrString("Release 2"),
	}
	service, probe, claim, claimNow := newExecutionProbe(t, proposal)
	expected := RequestResult{
		ApprovalID: "appr_1", ApplicantAgentID: "agt_1", RequestID: "req_1",
		Action: ActionUpdate, Status: terminalStatus,
	}
	if terminalStatus == RequestSucceeded {
		expected.RoleID = probe.applyResult.RoleID
		expected.RoleVersionID = probe.applyResult.RoleVersionID
	} else {
		probe.applyErr = &ApplyError{Outcome: ApplyVersionConflict}
		expected.FailureCode = string(ApplyVersionConflict)
		expected.FailureMessage = failureVersionConflictMessage
	}

	terminalMismatch := errors.New("terminal card content mismatch")
	var terminalCard *RequestResult
	projectedLoads := 0
	service.dependencies.LoadProjectedTerminal = func(
		_ context.Context,
		instance approval.Instance,
		payload ApprovalPayload,
	) (RequestResult, bool, error) {
		projectedLoads++
		require.Equal(t, probe.approval, instance)
		require.Equal(t, strictDecodePayloadForTest(t, probe.approval.Payload), payload)
		if terminalCard == nil {
			return RequestResult{}, false, nil
		}
		return *terminalCard, true, nil
	}
	projectCalls, cardMutations := 0, 0
	service.dependencies.ProjectTerminal = func(
		_ context.Context,
		instance approval.Instance,
		result RequestResult,
	) error {
		projectCalls++
		require.Equal(t, probe.approval, instance)
		if terminalCard != nil {
			if *terminalCard != result {
				return terminalMismatch
			}
			return nil
		}
		captured := result
		terminalCard = &captured
		cardMutations++
		return nil
	}

	injected := errors.New("injected settle failure after terminal card")
	settleAttempts, successfulSettles := 0, 0
	if terminalStatus == RequestSucceeded {
		service.dependencies.Succeed = func(
			ctx context.Context,
			fence executionstore.ExecutionFence,
			success executionstore.Success,
			now time.Time,
		) error {
			probe.recordSettle(ctx, "succeed", fence, executionstore.Failure{}, success, now)
			settleAttempts++
			require.Equal(t, probe.applyResult, success)
			if settleAttempts == 1 {
				return injected
			}
			successfulSettles++
			return nil
		}
	} else {
		service.dependencies.FailDeterministic = func(
			ctx context.Context,
			fence executionstore.ExecutionFence,
			failure executionstore.Failure,
			now time.Time,
		) error {
			probe.recordSettle(ctx, "fail", fence, failure, executionstore.Success{}, now)
			settleAttempts++
			require.Equal(t, deterministicFailure(ApplyVersionConflict), failure)
			if settleAttempts == 1 {
				return injected
			}
			successfulSettles++
			return nil
		}
	}

	require.ErrorIs(t, service.Execute(context.Background(), claim, claimNow), injected)
	require.NotNil(t, terminalCard)
	require.Equal(t, expected, *terminalCard)
	require.Equal(t, 1, projectCalls)
	require.Equal(t, 1, cardMutations)
	require.Equal(t, 1, projectedLoads)
	require.Equal(t, 1, countCall(probe.calls, "authority"))
	require.Equal(t, 1, countCall(probe.calls, "apply"))
	require.Equal(t, 1, settleAttempts)
	require.Zero(t, successfulSettles)

	if terminalStatus == RequestSucceeded {
		probe.authorityErr = &ApplyError{Outcome: ApplyAuthorityDenied}
	} else {
		probe.applyErr = nil
	}
	secondErr := service.Execute(context.Background(), claim, claimNow)

	assert.NoError(t, secondErr,
		"resolved terminal card must settle its original result despite dependency drift")
	assert.Equal(t, expected, *terminalCard, "resolved terminal result must remain stable")
	assert.Equal(t, 1, projectCalls, "settle retry must not re-project the terminal card")
	assert.Equal(t, 1, cardMutations, "settle retry must not remutate the terminal card")
	assert.Equal(t, 2, projectedLoads, "each valid attempt must load projected state once")
	assert.Equal(t, 1, countCall(probe.calls, "recover"),
		"settle retry must not look past an existing terminal card")
	assert.Equal(t, 1, countCall(probe.calls, "authority"),
		"settle retry must not recheck current authority")
	assert.Equal(t, 1, countCall(probe.calls, "apply"),
		"settle retry must not call MeshX again")
	assert.Equal(t, 2, settleAttempts)
	assert.Equal(t, 1, successfulSettles, "original terminal result must settle exactly once")
	if terminalStatus == RequestSucceeded {
		assert.Zero(t, countCall(probe.calls, "fail"))
	} else {
		assert.Zero(t, countCall(probe.calls, "succeed"))
	}
}

func testExecuteTerminalProjectionRetry(t *testing.T, mode string) {
	t.Helper()
	var proposal RoleProposal = RoleCreateProposal{Name: "Release", Definition: "Review releases"}
	expected := RequestResult{
		ApprovalID: "appr_1", ApplicantAgentID: "agt_1", RequestID: "req_1",
		Action: ActionCreate, Status: RequestSucceeded,
		RoleID: "role_1", RoleVersionID: "rlv_1",
	}
	if mode == "failure projection" {
		proposal = RoleUpdateProposal{
			RoleID: "role_1", ExpectedRoleVersionID: "rlv_expected",
			Name: ptrString("Release 2"),
		}
		expected = RequestResult{
			ApprovalID: "appr_1", ApplicantAgentID: "agt_1", RequestID: "req_1",
			Action: ActionUpdate, Status: RequestFailed,
			FailureCode: string(ApplyVersionConflict), FailureMessage: failureVersionConflictMessage,
		}
	} else if mode == "settle" {
		proposal = RoleApplyProposal{
			RoleID: "role_1", RoleVersionID: "rlv_1", AgentID: "agt_target",
		}
		expected.Action, expected.AgentID = ActionApply, "agt_target"
	}
	service, probe, claim, claimNow := newExecutionProbe(t, proposal)
	if mode == "failure projection" {
		probe.applyErr = &ApplyError{Outcome: ApplyVersionConflict}
	}
	injected := errors.New("injected terminal boundary failure")
	projectionAttempts, cardMutations := 0, 0
	cardTerminal := false
	var operationKeys, requestDigests []string
	service.dependencies.ProjectTerminal = func(
		_ context.Context, instance approval.Instance, result RequestResult,
	) error {
		probe.calls = append(probe.calls, "project")
		projectionAttempts++
		operationKeys = append(operationKeys, probe.applyCommand.OperationKey)
		requestDigests = append(requestDigests, probe.applyCommand.RequestDigest)
		require.Equal(t, probe.approval, instance)
		require.Equal(t, expected, result)
		require.NotContains(t, result.FailureMessage, executionSecretSentinel)
		if mode != "settle" && projectionAttempts == 1 {
			return injected
		}
		if !cardTerminal {
			cardTerminal = true
			cardMutations++
		}
		return nil
	}
	settleAttempts, successfulSettles := 0, 0
	if mode == "settle" {
		service.dependencies.Succeed = func(
			ctx context.Context, fence executionstore.ExecutionFence,
			success executionstore.Success, now time.Time,
		) error {
			probe.recordSettle(ctx, "succeed", fence, executionstore.Failure{}, success, now)
			settleAttempts++
			if settleAttempts == 1 {
				return injected
			}
			successfulSettles++
			return nil
		}
	}

	require.ErrorIs(t, service.Execute(context.Background(), claim, claimNow), injected)
	if mode == "settle" {
		require.Equal(t, 1, cardMutations)
		require.Equal(t, 1, settleAttempts)
		require.Zero(t, successfulSettles)
	} else {
		require.Equal(t, []string{"load", "recover", "authority", "apply", "project"}, probe.calls)
		require.Zero(t, totalSettles(probe), "projection must complete before settle")
	}
	require.NoError(t, service.Execute(context.Background(), claim, claimNow))
	require.Equal(t, 2, projectionAttempts, "retry must revalidate the terminal card")
	require.Equal(t, 1, cardMutations, "matching terminal content must not remutate")
	require.Len(t, operationKeys, 2)
	require.Equal(t, operationKeys[0], operationKeys[1])
	require.Equal(t, requestDigests[0], requestDigests[1])
	if mode == "settle" {
		require.Equal(t, 2, settleAttempts)
		require.Equal(t, 1, successfulSettles, "replayed receipt must settle exactly once")
	} else {
		require.Equal(t, 1, totalSettles(probe))
		require.Equal(t, mode == "failure projection", probe.settleFailures[0].Code != "")
	}
}
