package execution

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
)

func TestApprovalDecideWithEffectAtomicallyEnqueuesExecution(t *testing.T) {
	ctx := context.Background()
	db := startRoleManagementRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	suffix := testIdentifierSuffix(t.Name())
	workspaceID := "wsp_role_decide_" + suffix
	applicantID := "mem_role_decide_agent_" + suffix
	approverID := "mem_role_decide_human_" + suffix
	seedRoleDecisionActors(t, db, workspaceID, applicantID, approverID, now)

	approvalStore := approval.New(db)
	executionStore := New(db)

	t.Run("approve commits one queued row and replay skips the effect", func(t *testing.T) {
		instance, assignment := createPendingRoleApproval(
			t, ctx, approvalStore, workspaceID, applicantID, approverID,
			"appr_role_decide_approve_"+suffix, now,
		)
		params := approval.DecideParams{
			WorkspaceID:      workspaceID,
			ApprovalID:       instance.ID,
			AssignmentID:     assignment.ID,
			ActorMemberID:    approverID,
			ExpectedVersion:  instance.Version,
			ClientDecisionID: "role-decide-approve-" + suffix,
			Decision:         approval.DecisionApproved,
			Reason:           "approved",
			Now:              now.Add(time.Second),
		}
		effectCalls := 0
		effect := func(
			effectCtx context.Context,
			tx coredb.Tx,
			got approval.Instance,
			decision string,
		) (approval.DecisionEffectResult, error) {
			effectCalls++
			require.Equal(t, approval.StatePending, got.State)
			require.Equal(t, approval.DecisionApproved, decision)
			queued, created, err := executionStore.InTx(tx).Enqueue(
				effectCtx, got.WorkspaceID, got.ID, params.Now,
			)
			if err != nil {
				return approval.DecisionEffectResult{}, err
			}
			require.True(t, created)
			require.Equal(t, ExecutionQueued, queued.Status)
			return approval.DecisionEffectResult{}, nil
		}

		result, err := approvalStore.DecideWithEffect(ctx, params, effect)
		require.NoError(t, err)
		require.False(t, result.AlreadyApplied)
		require.Equal(t, approval.StateApproved, result.Instance.State)
		require.Equal(t, 1, effectCalls)

		queued, err := executionStore.LoadByApprovalID(ctx, workspaceID, instance.ID)
		require.NoError(t, err)
		require.Equal(t, ExecutionQueued, queued.Status)

		replayed, err := approvalStore.DecideWithEffect(ctx, params, effect)
		require.NoError(t, err)
		require.True(t, replayed.AlreadyApplied)
		require.Equal(t, 1, effectCalls, "decision replay must not run enqueue again")
		replayedExecution, err := executionStore.LoadByApprovalID(ctx, workspaceID, instance.ID)
		require.NoError(t, err)
		require.Equal(t, queued, replayedExecution)
	})

	t.Run("reject commits no execution row", func(t *testing.T) {
		instance, assignment := createPendingRoleApproval(
			t, ctx, approvalStore, workspaceID, applicantID, approverID,
			"appr_role_decide_reject_"+suffix, now,
		)
		result, err := approvalStore.DecideWithEffect(ctx, approval.DecideParams{
			WorkspaceID:      workspaceID,
			ApprovalID:       instance.ID,
			AssignmentID:     assignment.ID,
			ActorMemberID:    approverID,
			ExpectedVersion:  instance.Version,
			ClientDecisionID: "role-decide-reject-" + suffix,
			Decision:         approval.DecisionRejected,
			Reason:           "rejected",
			Now:              now.Add(2 * time.Second),
		}, func(
			_ context.Context,
			_ coredb.Tx,
			_ approval.Instance,
			decision string,
		) (approval.DecisionEffectResult, error) {
			require.Equal(t, approval.DecisionRejected, decision)
			return approval.DecisionEffectResult{}, nil
		})
		require.NoError(t, err)
		require.Equal(t, approval.StateRejected, result.Instance.State)
		_, err = executionStore.LoadByApprovalID(ctx, workspaceID, instance.ID)
		require.ErrorIs(t, err, ErrNotFound)
	})

	t.Run("effect failure rolls back both enqueue and Approval decision", func(t *testing.T) {
		instance, assignment := createPendingRoleApproval(
			t, ctx, approvalStore, workspaceID, applicantID, approverID,
			"appr_role_decide_rollback_"+suffix, now,
		)
		effectErr := errors.New("role execution effect failed")
		_, err := approvalStore.DecideWithEffect(ctx, approval.DecideParams{
			WorkspaceID:      workspaceID,
			ApprovalID:       instance.ID,
			AssignmentID:     assignment.ID,
			ActorMemberID:    approverID,
			ExpectedVersion:  instance.Version,
			ClientDecisionID: "role-decide-rollback-" + suffix,
			Decision:         approval.DecisionApproved,
			Reason:           "approved",
			Now:              now.Add(3 * time.Second),
		}, func(
			effectCtx context.Context,
			tx coredb.Tx,
			got approval.Instance,
			decision string,
		) (approval.DecisionEffectResult, error) {
			require.Equal(t, approval.DecisionApproved, decision)
			_, created, enqueueErr := executionStore.InTx(tx).Enqueue(
				effectCtx, got.WorkspaceID, got.ID, now.Add(3*time.Second),
			)
			if enqueueErr != nil {
				return approval.DecisionEffectResult{}, enqueueErr
			}
			require.True(t, created)
			return approval.DecisionEffectResult{}, effectErr
		})
		require.ErrorIs(t, err, effectErr)

		unchanged, err := approvalStore.Load(ctx, workspaceID, instance.ID)
		require.NoError(t, err)
		require.Equal(t, approval.StatePending, unchanged.State)
		_, err = executionStore.LoadByApprovalID(ctx, workspaceID, instance.ID)
		require.ErrorIs(t, err, ErrNotFound)
	})

	t.Run("Approval terminal write failure rolls back enqueue", func(t *testing.T) {
		instance, assignment := createPendingRoleApproval(
			t, ctx, approvalStore, workspaceID, applicantID, approverID,
			"appr_role_decide_terminal_rollback_"+suffix, now,
		)
		_, err := approvalStore.DecideWithEffect(ctx, approval.DecideParams{
			WorkspaceID:      workspaceID,
			ApprovalID:       instance.ID,
			AssignmentID:     assignment.ID,
			ActorMemberID:    approverID,
			ExpectedVersion:  instance.Version,
			ClientDecisionID: "role-decide-terminal-rollback-" + suffix,
			Decision:         approval.DecisionApproved,
			Reason:           "approved",
			Now:              now.Add(4 * time.Second),
		}, func(
			effectCtx context.Context,
			tx coredb.Tx,
			got approval.Instance,
			decision string,
		) (approval.DecisionEffectResult, error) {
			require.Equal(t, approval.DecisionApproved, decision)
			_, created, enqueueErr := executionStore.InTx(tx).Enqueue(
				effectCtx, got.WorkspaceID, got.ID, now.Add(4*time.Second),
			)
			if enqueueErr != nil {
				return approval.DecisionEffectResult{}, enqueueErr
			}
			require.True(t, created)
			// Force the generic terminal CAS that follows the effect to lose.
			_, updateErr := tx.Exec(effectCtx, `
UPDATE approval_instances
SET version=version+1
WHERE workspace_id=$1 AND id=$2`, got.WorkspaceID, got.ID)
			return approval.DecisionEffectResult{}, updateErr
		})
		require.Error(t, err)

		unchanged, err := approvalStore.Load(ctx, workspaceID, instance.ID)
		require.NoError(t, err)
		require.Equal(t, approval.StatePending, unchanged.State)
		require.Equal(t, instance.Version, unchanged.Version)
		_, err = executionStore.LoadByApprovalID(ctx, workspaceID, instance.ID)
		require.ErrorIs(t, err, ErrNotFound)
	})
}

func createPendingRoleApproval(
	t testing.TB,
	ctx context.Context,
	store *approval.Store,
	workspaceID, applicantID, approverID, approvalID string,
	now time.Time,
) (approval.Instance, approval.Assignment) {
	t.Helper()
	payload := []byte(`{"schemaVersion":1}`)
	digest := sha256.Sum256(payload)
	instance, assignments, created, err := store.Create(ctx, approval.CreateParams{
		ID:                  approvalID,
		WorkspaceID:         workspaceID,
		SubjectType:         "role_management",
		SubjectRef:          approvalID,
		SubjectVersion:      1,
		Payload:             payload,
		PayloadDigest:       hex.EncodeToString(digest[:]),
		IdempotencyKey:      testIdentifierSuffix("idempotency-" + approvalID),
		PendingKey:          testIdentifierSuffix("pending-" + approvalID),
		ApplicantMemberID:   applicantID,
		AllowAgentApplicant: true,
		ApproverMemberIDs:   []string{approverID},
		Now:                 now,
	})
	require.NoError(t, err)
	require.True(t, created)
	require.Len(t, assignments, 1)
	require.Equal(t, approval.StatePending, instance.State)
	return instance, assignments[0]
}

func seedRoleDecisionActors(
	t testing.TB,
	db coredb.QueryExecer,
	workspaceID, applicantID, approverID string,
	now time.Time,
) {
	t.Helper()
	userID := "usr_" + approverID
	orgID := "org_" + workspaceID
	mustExecRoleManagement(t, db, `
INSERT INTO users (id,email,name,display_name,password_hash,created_at,updated_at)
VALUES ($1,$2,$1,$1,'hash',$3,$3)`,
		userID, userID+"@example.com", now)
	mustExecRoleManagement(t, db, `
INSERT INTO organizations (id,slug,name,created_by_user_id,created_at,updated_at)
VALUES ($1,$1,$1,$2,$3,$3)`, orgID, userID, now)
	mustExecRoleManagement(t, db, `
INSERT INTO workspaces (id,organization_id,slug,name,created_at,updated_at)
VALUES ($1,$2,$1,$1,$3,$3)`, workspaceID, orgID, now)
	mustExecRoleManagement(t, db, `
INSERT INTO members (id,workspace_id,type,display_name,status,created_at,updated_at)
VALUES ($1,$2,'agent',$1,'active',$3,$3)`, applicantID, workspaceID, now)
	mustExecRoleManagement(t, db, `
INSERT INTO members (
  id,workspace_id,type,display_name,user_id,workspace_role,status,created_at,updated_at
) VALUES ($1,$2,'human',$1,$3,'admin','active',$4,$4)`,
		approverID, workspaceID, userID, now)
}
