package execution

import (
	"context"
	"errors"
	"testing"

	"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 TestExecuteResponseLostRetryRecoversCommittedReceiptBeforeAuthorityDrift(t *testing.T) {
	service, probe, claim, claimNow := newExecutionProbe(
		t,
		RoleApplyProposal{RoleID: "role_1", RoleVersionID: "rlv_1", AgentID: "agt_target"},
	)
	receiptCommitted := false
	service.dependencies.Apply = func(_ context.Context, command ApplyCommand) (executionstore.Success, error) {
		probe.calls = append(probe.calls, "apply")
		probe.applyCommand = command
		if !receiptCommitted {
			receiptCommitted = true
			return executionstore.Success{}, &ApplyError{Outcome: ApplyResponseLost}
		}
		return probe.applyResult, nil
	}

	require.NoError(t, service.Execute(context.Background(), claim, claimNow))
	require.True(t, receiptCommitted)
	require.Equal(t, []string{"load", "recover", "authority", "apply", "now", "release"}, probe.calls)
	requireSingleSettle(t, probe, claim, claimNow, "release")

	authorityCalls := countCall(probe.calls, "authority")
	applyCalls := countCall(probe.calls, "apply")
	probe.recover = probe.applyResult
	probe.recoverFound = true
	probe.authority.OwnedAgentIDs = []string{"agt_other"}
	require.NoError(t, service.Execute(context.Background(), claim, claimNow))

	assert.Equal(t, 2, countCall(probe.calls, "recover"))
	assert.Equal(t, authorityCalls, countCall(probe.calls, "authority"),
		"receipt recovery must run before current authority")
	assert.Equal(t, applyCalls, countCall(probe.calls, "apply"),
		"receipt recovery must not execute the mutation again")
	assert.Zero(t, countCall(probe.calls, "fail"),
		"a committed successful receipt must not be rewritten as a deterministic failure")
	assert.Equal(t, 1, countCall(probe.calls, "succeed"))
	if assert.Len(t, probe.settleSuccesses, 2) {
		assert.Equal(t, probe.applyResult, probe.settleSuccesses[1])
	}
}

func TestExecuteRecoversAppliedReceiptBeforeAuthorityForEveryAction(t *testing.T) {
	tests := []struct {
		name     string
		proposal RoleProposal
	}{
		{
			name:     "create",
			proposal: RoleCreateProposal{Name: "Release", Definition: "Review releases"},
		},
		{
			name: "update",
			proposal: RoleUpdateProposal{
				RoleID: "role_1", ExpectedRoleVersionID: "rlv_old",
				Name: ptrString("Release 2"),
			},
		},
		{
			name: "delete",
			proposal: RoleDeleteProposal{
				RoleID: "role_1", ExpectedRoleVersionID: "rlv_old",
			},
		},
		{
			name: "apply",
			proposal: RoleApplyProposal{
				RoleID: "role_1", RoleVersionID: "rlv_1", AgentID: "agt_target",
			},
		},
	}
	for _, testCase := range tests {
		t.Run(testCase.name, func(t *testing.T) {
			service, probe, claim, claimNow := newExecutionProbe(t, testCase.proposal)
			probe.recover = executionstore.Success{
				RoleID: "role_stable", RoleVersionID: "rlv_stable",
			}
			probe.recoverFound = true
			var projected RequestResult
			service.dependencies.ProjectTerminal = func(
				_ context.Context,
				instance approval.Instance,
				result RequestResult,
			) error {
				require.Equal(t, probe.approval, instance)
				projected = result
				return nil
			}

			require.NoError(t, service.Execute(context.Background(), claim, claimNow))
			require.Equal(t, []string{"load", "recover", "now", "succeed"}, probe.calls)
			require.Equal(t, probe.approval, probe.recoverCommand.Approval)
			require.Equal(t, strictDecodePayloadForTest(t, probe.approval.Payload), probe.recoverCommand.Payload)
			require.Equal(t, probe.approval.ID, probe.recoverCommand.OperationKey)
			require.Equal(t, probe.approval.PayloadDigest, probe.recoverCommand.RequestDigest)
			require.Equal(t, RequestSucceeded, projected.Status)
			require.Equal(t, probe.recover.RoleID, projected.RoleID)
			require.Equal(t, probe.recover.RoleVersionID, projected.RoleVersionID)
			requireSingleSettle(t, probe, claim, claimNow, "succeed")
			require.Equal(t, probe.recover, probe.settleSuccesses[0])
		})
	}
}

func TestExecuteRecoverAppliedMissConflictAndUncertain(t *testing.T) {
	t.Run("miss continues current authority and apply", func(t *testing.T) {
		service, probe, claim, claimNow := newExecutionProbe(
			t, RoleCreateProposal{Name: "Release", Definition: "Review releases"},
		)

		require.NoError(t, service.Execute(context.Background(), claim, claimNow))
		require.Equal(t, []string{"load", "recover", "authority", "apply", "now", "succeed"}, probe.calls)
		requireSingleSettle(t, probe, claim, claimNow, "succeed")
	})

	t.Run("action or digest conflict fails deterministically", func(t *testing.T) {
		service, probe, claim, claimNow := newExecutionProbe(
			t, RoleCreateProposal{Name: "Release", Definition: "Review releases"},
		)
		probe.recoverErr = &ApplyError{Outcome: ApplyOperationConflict}

		require.NoError(t, service.Execute(context.Background(), claim, claimNow))
		require.Equal(t, []string{"load", "recover", "now", "fail"}, probe.calls)
		requireSafeFailure(t, probe.settleFailures[0], string(ApplyOperationConflict))
		requireSingleSettle(t, probe, claim, claimNow, "fail")
	})

	for _, testCase := range []struct {
		name   string
		result executionstore.Success
		found  bool
		err    error
	}{
		{name: "lookup error", err: errors.New("transport unavailable")},
		{name: "incomplete role id", result: executionstore.Success{RoleVersionID: "rlv_1"}, found: true},
		{name: "incomplete version id", result: executionstore.Success{RoleID: "role_1"}, found: true},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			service, probe, claim, claimNow := newExecutionProbe(
				t, RoleCreateProposal{Name: "Release", Definition: "Review releases"},
			)
			probe.recover = testCase.result
			probe.recoverFound = testCase.found
			probe.recoverErr = testCase.err

			require.NoError(t, service.Execute(context.Background(), claim, claimNow))
			require.Equal(t, []string{"load", "recover", "now", "release"}, probe.calls)
			requireSingleSettle(t, probe, claim, claimNow, "release")
		})
	}
}
