package execution

import (
	"context"
	"crypto/sha256"
	"errors"
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func requireExecutionStoreImplemented(t *testing.T, err error) {
	t.Helper()
	if errors.Is(err, ErrNotImplemented) {
		t.Fatalf("RED: %v", err)
	}
	require.NoError(t, err)
}

func startExecutionStore(t *testing.T) (*Store, string, string, string, time.Time) {
	t.Helper()
	ctx := context.Background()
	db := startRoleManagementRollbackDB(t, ctx)
	now := time.Now().UTC().Truncate(time.Microsecond)
	suffix := testIdentifierSuffix(t.Name())
	workspaceID := "wsp_role_management_" + suffix
	memberID := "mem_role_management_" + suffix
	approvalID := "appr_role_management_" + suffix
	seedExecutionApproval(t, db, workspaceID, memberID, approvalID, now)
	return New(db), workspaceID, approvalID, memberID, now
}

func enqueueExecution(t *testing.T, ctx context.Context, store *Store, workspaceID, approvalID string, now time.Time) Execution {
	t.Helper()
	var value Execution
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var created bool
		var err error
		value, created, err = store.InTx(tx).Enqueue(ctx, workspaceID, approvalID, now)
		require.True(t, created)
		return err
	})
	requireExecutionStoreImplemented(t, err)
	return value
}

func TestExecutionCarriesOnlyApprovalLinkAndExecutionFacts(t *testing.T) {
	typ := reflect.TypeOf(Execution{})
	for _, required := range []string{
		"ApprovalID", "WorkspaceID", "Status", "LeaseToken", "LeaseExpiresAt",
		"ResultRoleID", "ResultRoleVersionID", "FailureCode", "FailureMessage",
		"CreatedAt", "UpdatedAt",
	} {
		_, ok := typ.FieldByName(required)
		require.Truef(t, ok, "execution must retain %s", required)
	}
	for _, forbidden := range []string{
		"ID", "Action", "Payload", "PayloadDigest", "RequestID",
		"ApplicantAgentID", "ApplicantMemberID", "OwnerID", "ManagerID",
		"AgentID", "CorrelationID", "LeaseOwner", "Attempt", "Revision",
	} {
		_, ok := typ.FieldByName(forbidden)
		require.Falsef(t, ok, "Approval is the sole ledger; execution must not copy %s", forbidden)
	}
}

func TestExecutionStoreRequiresRealCoreDBWithoutFallback(t *testing.T) {
	ctx := context.Background()
	store := New(nil)

	_, err := store.LoadByApprovalID(ctx, "ws_1", "appr_1")
	require.Error(t, err)
	require.NotErrorIs(t, err, ErrNotImplemented)
	_, err = store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: "ws_1", Now: time.Now(), LeaseTTL: time.Minute, Limit: 1,
	})
	require.Error(t, err)
	require.NotErrorIs(t, err, ErrNotImplemented)
	_, _, err = store.InTx(nil).Enqueue(ctx, "ws_1", "appr_1", time.Now())
	require.Error(t, err)
	require.NotErrorIs(t, err, ErrNotImplemented)
}

func TestEnqueueIsOneRowPerApprovalAndWorkspaceFenced(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, _, now := startExecutionStore(t)

	var first Execution
	var created bool
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		first, created, err = store.InTx(tx).Enqueue(ctx, workspaceID, approvalID, now)
		return err
	})
	requireExecutionStoreImplemented(t, err)
	require.True(t, created)
	require.Equal(t, Execution{
		ApprovalID: approvalID, WorkspaceID: workspaceID, Status: ExecutionQueued,
		CreatedAt: now, UpdatedAt: now,
	}, first)

	var replayed Execution
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		replayed, created, err = store.InTx(tx).Enqueue(ctx, workspaceID, approvalID, now.Add(time.Second))
		return err
	})
	requireExecutionStoreImplemented(t, err)
	require.False(t, created)
	require.Equal(t, first, replayed)

	loaded, err := store.LoadByApprovalID(ctx, workspaceID, approvalID)
	requireExecutionStoreImplemented(t, err)
	require.Equal(t, first, loaded)
	_, err = store.LoadByApprovalID(ctx, "ws_other", approvalID)
	require.ErrorIs(t, err, ErrNotFound)
}

func TestEnqueueRequiresPendingRoleManagementApproval(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, memberID, now := startExecutionStore(t)

	for _, testCase := range []struct {
		name       string
		approvalID string
		mutate     string
	}{
		{
			name:       "approved Approval",
			approvalID: approvalID + "_approved",
			mutate: `
UPDATE approval_instances
SET state='approved',decision='approved',decided_by_member_id=$3,decided_at=$4
WHERE workspace_id=$1 AND id=$2`,
		},
		{
			name:       "unrelated Approval subject",
			approvalID: approvalID + "_other_subject",
			mutate: `
UPDATE approval_instances
SET subject_type='unrelated_subject'
WHERE workspace_id=$1 AND id=$2`,
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			seedExecutionWorkspaceAndApproval(
				t, store.db, workspaceID+"_"+testIdentifierSuffix(testCase.name),
				memberID+"_"+testIdentifierSuffix(testCase.name), testCase.approvalID, now,
			)
			var testWorkspaceID string
			require.NoError(t, store.db.QueryRow(ctx, `
SELECT workspace_id FROM approval_instances WHERE id=$1`, testCase.approvalID).Scan(&testWorkspaceID))
			args := []any{testWorkspaceID, testCase.approvalID}
			if testCase.name == "approved Approval" {
				args = append(args, memberID+"_"+testIdentifierSuffix(testCase.name), now)
			}
			_, err := store.db.Exec(ctx, testCase.mutate, args...)
			require.NoError(t, err)

			err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
				_, _, enqueueErr := store.InTx(tx).Enqueue(
					ctx, testWorkspaceID, testCase.approvalID, now,
				)
				return enqueueErr
			})
			require.ErrorIs(t, err, ErrNotFound)
		})
	}
}

func TestExpiredExecutionLeaseIsReclaimedWithANewFence(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, _, now := startExecutionStore(t)
	enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))

	first, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	require.Len(t, first, 1)
	require.Equal(t, ExecutionRunning, first[0].Execution.Status)

	reclaimed, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now.Add(61 * time.Second), LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	require.Len(t, reclaimed, 1)
	require.Equal(t, first[0].Execution.ApprovalID, reclaimed[0].Execution.ApprovalID)
	require.NotEqual(t, first[0].Fence.LeaseToken, reclaimed[0].Fence.LeaseToken)
}

func TestClaimRunnableIsWorkspaceScoped(t *testing.T) {
	ctx := context.Background()
	store, workspace2, approval2, memberID, now := startExecutionStore(t)
	db := store.db

	// Put another tenant first so a claim implementation that ignores
	// WorkspaceID cannot accidentally pass by global FIFO order.
	enqueueExecution(t, ctx, store, workspace2, approval2, now.Add(-time.Minute))
	ws1 := workspace2 + "_other"
	approval1 := approval2 + "_other"
	seedExecutionWorkspaceAndApproval(t, db, ws1, memberID+"_other", approval1, now)
	none, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: ws1, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	require.Empty(t, none, "ws_1 claim must not take the only queued ws_2 row")

	claimedWS2, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspace2, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	require.Len(t, claimedWS2, 1)
	require.Equal(t, approval2, claimedWS2[0].Execution.ApprovalID)

	enqueueExecution(t, ctx, store, ws1, approval1, now.Add(-time.Minute))
	claimedWS1, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: ws1, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	require.Len(t, claimedWS1, 1)
	require.Equal(t, ws1, claimedWS1[0].Execution.WorkspaceID)
	require.Equal(t, approval1, claimedWS1[0].Execution.ApprovalID)

	noOtherTenant, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspace2, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	require.Empty(t, noOtherTenant, "ws_2 claim must not take ws_1's live row")
}

func testIdentifierSuffix(value string) string {
	sum := sha256.Sum256([]byte(value))
	return fmt.Sprintf("%x", sum[:8])
}

func TestClaimRunnableIsExclusiveAndSkipsLiveOrTerminalRows(t *testing.T) {
	t.Run("same queued row same instant has one winning token", func(t *testing.T) {
		ctx := context.Background()
		store, workspaceID, approvalID, _, now := startExecutionStore(t)
		enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))

		first, err := store.ClaimRunnable(ctx, ClaimParams{
			WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
		})
		requireExecutionStoreImplemented(t, err)
		require.Len(t, first, 1)
		require.NotEmpty(t, first[0].Fence.LeaseToken)

		second, err := store.ClaimRunnable(ctx, ClaimParams{
			WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
		})
		requireExecutionStoreImplemented(t, err)
		require.Empty(t, second,
			"the live running row cannot produce a second token at the same instant")
	})

	for _, terminal := range []ExecutionStatus{ExecutionSucceeded, ExecutionFailed} {
		t.Run(string(terminal), func(t *testing.T) {
			ctx := context.Background()
			store, workspaceID, approvalID, _, now := startExecutionStore(t)
			enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))
			claimed, err := store.ClaimRunnable(ctx, ClaimParams{
				WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
			})
			requireExecutionStoreImplemented(t, err)
			require.Len(t, claimed, 1)
			switch terminal {
			case ExecutionSucceeded:
				_, err = store.Succeed(ctx, claimed[0].Fence, Success{RoleID: "role_1"}, now)
			case ExecutionFailed:
				_, err = store.FailDeterministic(ctx, claimed[0].Fence, Failure{Code: "denied"}, now)
			}
			requireExecutionStoreImplemented(t, err)

			again, err := store.ClaimRunnable(ctx, ClaimParams{
				WorkspaceID: workspaceID, Now: now.Add(time.Hour), LeaseTTL: time.Minute, Limit: 1,
			})
			requireExecutionStoreImplemented(t, err)
			require.Empty(t, again, "terminal executions are never reclaimable")
		})
	}
}

func TestUncertainFailureRequeuesSameApprovalWithoutTerminalFailure(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, _, now := startExecutionStore(t)
	enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))
	claimed, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)

	got, err := store.ReleaseUncertain(ctx, claimed[0].Fence, now.Add(10*time.Second))
	requireExecutionStoreImplemented(t, err)
	require.Equal(t, ExecutionQueued, got.Status)
	require.Equal(t, approvalID, got.ApprovalID)
	require.Empty(t, got.FailureCode)
	require.Empty(t, got.LeaseToken)
	require.Nil(t, got.LeaseExpiresAt)
}

func TestCrashAfterExternalSuccessCannotBeRecordedAsFailedAndIsReclaimed(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, _, now := startExecutionStore(t)
	enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))
	first, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)

	// Model MeshX success followed by process death before Core settlement: no
	// terminal method is called. The durable row must remain non-failed and
	// become reclaimable after its lease expires.
	duringCrash, err := store.LoadByApprovalID(ctx, workspaceID, approvalID)
	requireExecutionStoreImplemented(t, err)
	require.Equal(t, ExecutionRunning, duringCrash.Status)
	require.Empty(t, duringCrash.FailureCode)

	reclaimed, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now.Add(61 * time.Second), LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	require.Len(t, reclaimed, 1)
	require.Equal(t, first[0].Execution.ApprovalID, reclaimed[0].Execution.ApprovalID)
	require.NotEqual(t, first[0].Fence.LeaseToken, reclaimed[0].Fence.LeaseToken)
}

func TestOnlyDeterministicFailureMayBecomeTerminal(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, _, now := startExecutionStore(t)
	enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))
	claimed, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)

	got, err := store.FailDeterministic(ctx, claimed[0].Fence, Failure{
		Code: "stale_role_version", Message: "Role version changed",
	}, now.Add(10*time.Second))
	requireExecutionStoreImplemented(t, err)
	require.Equal(t, ExecutionFailed, got.Status)
	require.Equal(t, "stale_role_version", got.FailureCode)
	require.Empty(t, got.LeaseToken)
	require.Nil(t, got.LeaseExpiresAt)
}

func TestSuccessStoresOnlyStableRoleIDsAndClearsLease(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, _, now := startExecutionStore(t)
	enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))
	claimed, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)

	got, err := store.Succeed(ctx, claimed[0].Fence, Success{
		RoleID: "role_1", RoleVersionID: "rlv_1",
	}, now.Add(10*time.Second))
	requireExecutionStoreImplemented(t, err)
	require.Equal(t, ExecutionSucceeded, got.Status)
	require.Equal(t, "role_1", got.ResultRoleID)
	require.Equal(t, "rlv_1", got.ResultRoleVersionID)
	require.Empty(t, got.LeaseToken)
	require.Nil(t, got.LeaseExpiresAt)
}

func TestStaleLeaseTokenCannotSettleOrReleaseExecution(t *testing.T) {
	ctx := context.Background()
	store, workspaceID, approvalID, _, now := startExecutionStore(t)
	enqueueExecution(t, ctx, store, workspaceID, approvalID, now.Add(-time.Minute))
	claimed, err := store.ClaimRunnable(ctx, ClaimParams{
		WorkspaceID: workspaceID, Now: now, LeaseTTL: time.Minute, Limit: 1,
	})
	requireExecutionStoreImplemented(t, err)
	stale := claimed[0].Fence
	stale.LeaseToken = "expired"

	_, err = store.Succeed(ctx, stale, Success{RoleID: "role_1"}, now.Add(time.Second))
	if errors.Is(err, ErrNotImplemented) {
		t.Fatalf("RED: %v", err)
	}
	require.ErrorIs(t, err, ErrConflict)

	_, err = store.FailDeterministic(ctx, stale, Failure{Code: "denied"}, now.Add(time.Second))
	require.ErrorIs(t, err, ErrConflict)
	_, err = store.ReleaseUncertain(ctx, stale, now.Add(time.Second))
	require.ErrorIs(t, err, ErrConflict)
}
