package agentops

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

var errLifecycleFakeUnexpected = errors.New("unexpected fake db call")

// lifecycleFakeTx models both the request-selector reservation row and the
// historical receipt ledger. It is a logic harness — it cannot reproduce TiDB
// transaction semantics, which the DB-backed concurrency test covers.
type lifecycleFakeTx struct {
	row                *LifecycleReceipt
	selector           *lifecycleRequestSelector
	insertErr          error
	updateRowsAffected *int64
	insertCalls        int
	selectorCalls      int
	updateCalls        []lifecycleFakeUpdate
	loadCalls          int
}

type lifecycleFakeUpdate struct {
	state        string
	resultStatus string
}

func lifecycleArg(args []any, index int) string {
	if index < 0 || index >= len(args) {
		return ""
	}
	value, ok := args[index].(string)
	if !ok {
		return ""
	}
	return value
}

func (tx *lifecycleFakeTx) QueryRow(_ context.Context, query string, _ ...any) coredb.Row {
	if strings.Contains(query, "FROM agent_lifecycle_request_selectors") {
		tx.loadCalls++
		if tx.selector != nil {
			return lifecycleFakeRow{selector: tx.selector}
		}
		return lifecycleFakeRow{err: sql.ErrNoRows}
	}
	if !strings.Contains(query, "FROM agent_lifecycle_receipts") {
		return lifecycleFakeRow{err: errLifecycleFakeUnexpected}
	}
	tx.loadCalls++
	if tx.row != nil {
		return lifecycleFakeRow{rec: tx.row}
	}
	return lifecycleFakeRow{err: sql.ErrNoRows}
}

func (tx *lifecycleFakeTx) Exec(_ context.Context, query string, args ...any) (coredb.Result, error) {
	switch {
	case strings.Contains(query, "INSERT INTO agent_lifecycle_request_selectors"):
		tx.selectorCalls++
		if tx.insertErr != nil {
			return nil, tx.insertErr
		}
		if tx.selector != nil {
			return lifecycleFakeResult(0), nil
		}
		if tx.row != nil {
			tx.selector = &lifecycleRequestSelector{
				ApplicantAgentID: tx.row.ApplicantAgentID, RequestID: tx.row.RequestID,
				Action: tx.row.Action, TargetAgentID: tx.row.TargetAgentID,
				PayloadDigest: tx.row.PayloadDigest,
			}
			return lifecycleFakeResult(0), nil
		}
		tx.selector = &lifecycleRequestSelector{
			ApplicantAgentID: lifecycleArg(args, 0), RequestID: lifecycleArg(args, 1),
			Action: lifecycleArg(args, 2), TargetAgentID: lifecycleArg(args, 3),
			PayloadDigest: lifecycleArg(args, 4),
		}
		return lifecycleFakeResult(1), nil
	case strings.Contains(query, "INSERT INTO agent_lifecycle_receipts"):
		tx.insertCalls++
		if tx.insertErr != nil {
			return nil, tx.insertErr
		}
		if tx.row != nil {
			// ON DUPLICATE KEY no-op: the existing (winner's) row stays; RowsAffected=0.
			return lifecycleFakeResult(0), nil
		}
		tx.row = &LifecycleReceipt{
			WorkspaceID:      lifecycleArg(args, 0),
			ApplicantAgentID: lifecycleArg(args, 1),
			OwnerUserID:      lifecycleArg(args, 2),
			Action:           lifecycleArg(args, 3),
			TargetAgentID:    lifecycleArg(args, 4),
			RequestID:        lifecycleArg(args, 5),
			PayloadDigest:    lifecycleArg(args, 6),
			State:            lifecycleArg(args, 7),
			CorrelationID:    lifecycleArg(args, 8),
		}
		return lifecycleFakeResult(1), nil
	case strings.Contains(query, "UPDATE agent_lifecycle_receipts"):
		update := lifecycleFakeUpdate{}
		if strings.Contains(query, "result_status = $6") {
			if v, ok := args[4].(string); ok {
				update.state = v
			}
			if v, ok := args[5].(string); ok {
				update.resultStatus = v
			}
		} else if len(args) >= 4 {
			if v, ok := args[3].(string); ok {
				update.state = v
			}
		}
		tx.updateCalls = append(tx.updateCalls, update)
		affected := int64(1)
		if tx.updateRowsAffected != nil {
			affected = *tx.updateRowsAffected
		}
		return lifecycleFakeResult(affected), nil
	}
	return nil, errLifecycleFakeUnexpected
}

func (tx *lifecycleFakeTx) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return nil, errLifecycleFakeUnexpected
}
func (tx *lifecycleFakeTx) Commit(context.Context) error   { return nil }
func (tx *lifecycleFakeTx) Rollback(context.Context) error { return nil }

type lifecycleFakeDB struct{ tx *lifecycleFakeTx }

func (db *lifecycleFakeDB) BeginTx(context.Context, coredb.TxOptions) (coredb.Tx, error) {
	return db.tx, nil
}
func (db *lifecycleFakeDB) QueryRow(ctx context.Context, query string, args ...any) coredb.Row {
	return db.tx.QueryRow(ctx, query, args...)
}
func (db *lifecycleFakeDB) Exec(ctx context.Context, query string, args ...any) (coredb.Result, error) {
	return db.tx.Exec(ctx, query, args...)
}
func (db *lifecycleFakeDB) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return nil, errLifecycleFakeUnexpected
}

type lifecycleFakeRow struct {
	rec      *LifecycleReceipt
	selector *lifecycleRequestSelector
	err      error
}

func (row lifecycleFakeRow) Scan(dest ...any) error {
	if row.err != nil {
		return row.err
	}
	if row.selector != nil {
		assignString(dest[0], row.selector.ApplicantAgentID)
		assignString(dest[1], row.selector.RequestID)
		assignNullableString(dest[2], row.selector.Action)
		assignNullableString(dest[3], row.selector.TargetAgentID)
		assignNullableString(dest[4], row.selector.PayloadDigest)
		if pointer, ok := dest[5].(*sql.NullInt64); ok {
			pointer.Int64, pointer.Valid = row.selector.ReceiptID, row.selector.ReceiptID != 0
		}
		if pointer, ok := dest[6].(*int); ok {
			if row.selector.LegacyAmbiguous {
				*pointer = 1
			}
		}
		return nil
	}
	rec := row.rec
	assignString(dest[0], rec.WorkspaceID)
	assignString(dest[1], rec.ApplicantAgentID)
	assignString(dest[2], rec.OwnerUserID)
	assignString(dest[3], rec.Action)
	assignString(dest[4], rec.TargetAgentID)
	assignString(dest[5], rec.RequestID)
	assignString(dest[6], rec.PayloadDigest)
	assignString(dest[7], rec.State)
	assignString(dest[8], rec.ResultStatus)
	assignString(dest[9], rec.CorrelationID)
	if pointer, ok := dest[10].(*time.Time); ok {
		*pointer = rec.CreatedAt
	}
	if pointer, ok := dest[11].(*time.Time); ok {
		*pointer = rec.UpdatedAt
	}
	return nil
}

func assignString(dest any, value string) {
	if pointer, ok := dest.(*string); ok {
		*pointer = value
	}
}

func assignNullableString(dest any, value string) {
	if pointer, ok := dest.(*sql.NullString); ok {
		pointer.String, pointer.Valid = value, value != ""
	}
}

type lifecycleFakeResult int64

func (result lifecycleFakeResult) RowsAffected() int64 { return int64(result) }

func lifecycleInt64(value int64) *int64 { return &value }

func validLifecycleIntent(digest string) LifecycleReceiptIntent {
	return LifecycleReceiptIntent{
		WorkspaceID: "wsp-1", ApplicantAgentID: "agt-caller", OwnerUserID: "usr-owner",
		Action: LifecycleActionStart, TargetAgentID: "agt-target", RequestID: "req-1",
		PayloadDigest: digest, CorrelationID: "corr-1",
	}
}

func TestReserveLifecycleReceiptInsertsWhenAbsent(t *testing.T) {
	t.Parallel()

	tx := &lifecycleFakeTx{}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	receipt, replayed, err := store.ReserveLifecycleReceipt(context.Background(), validLifecycleIntent("digest-a"))

	require.NoError(t, err)
	require.False(t, replayed)
	require.Equal(t, LifecycleReceiptStatePrepared, receipt.State)
	require.Equal(t, "digest-a", receipt.PayloadDigest)
	require.Equal(t, 1, tx.insertCalls, "absent reserve upserts exactly once")
}

func TestReserveLifecycleReceiptReplaysSamePayload(t *testing.T) {
	t.Parallel()

	existing := &LifecycleReceipt{
		WorkspaceID: "wsp-1", ApplicantAgentID: "agt-caller", OwnerUserID: "usr-owner",
		Action: LifecycleActionStart, TargetAgentID: "agt-target", RequestID: "req-1",
		PayloadDigest: "digest-a", State: LifecycleReceiptStateSucceeded,
		ResultStatus: "started", CorrelationID: "corr-1",
	}
	tx := &lifecycleFakeTx{row: existing}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	receipt, replayed, err := store.ReserveLifecycleReceipt(context.Background(), validLifecycleIntent("digest-a"))

	require.NoError(t, err)
	require.True(t, replayed, "same payload against an existing receipt replays")
	require.Equal(t, LifecycleReceiptStateSucceeded, receipt.State)
	require.Equal(t, 0, tx.insertCalls, "replay must not insert a second receipt")
	require.Equal(t, 1, tx.selectorCalls, "replay still locks the action-independent selector")
}

func TestReserveLifecycleReceiptConflictsOnDifferentPayload(t *testing.T) {
	t.Parallel()

	existing := &LifecycleReceipt{
		WorkspaceID: "wsp-1", ApplicantAgentID: "agt-caller", OwnerUserID: "usr-owner",
		Action: LifecycleActionStart, TargetAgentID: "agt-target", RequestID: "req-1",
		PayloadDigest: "digest-a", State: LifecycleReceiptStateSucceeded,
	}
	tx := &lifecycleFakeTx{row: existing}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	_, _, err := store.ReserveLifecycleReceipt(context.Background(), validLifecycleIntent("digest-b"))

	require.ErrorIs(t, err, ErrLifecyclePayloadConflict)
}

func TestReserveLifecycleReceiptConflictsWhenRequestIdChangesAction(t *testing.T) {
	t.Parallel()

	tx := &lifecycleFakeTx{}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})
	_, _, err := store.ReserveLifecycleReceipt(context.Background(), validLifecycleIntent("start-digest"))
	require.NoError(t, err)
	changed := validLifecycleIntent("stop-digest")
	changed.Action = LifecycleActionStop
	_, _, err = store.ReserveLifecycleReceipt(context.Background(), changed)
	require.ErrorIs(t, err, ErrLifecyclePayloadConflict)
	require.Equal(t, 1, tx.insertCalls, "cross-action conflict must not insert a second receipt")
	require.Equal(t, 2, tx.selectorCalls, "cross-action replay still probes the durable selector")
	require.Equal(t, LifecycleActionStart, tx.row.Action, "cross-action requestId must retain the original receipt row")
}

func TestReserveLifecycleReceiptReplaysLegacyAmbiguousActionOnlyExactly(t *testing.T) {
	t.Parallel()

	legacy := &LifecycleReceipt{
		WorkspaceID: "wsp-1", ApplicantAgentID: "agt-caller", OwnerUserID: "usr-owner",
		Action: LifecycleActionStart, TargetAgentID: "agt-target", RequestID: "req-legacy",
		PayloadDigest: "digest-start", State: LifecycleReceiptStateSucceeded, ResultStatus: "started",
	}
	tx := &lifecycleFakeTx{
		row: legacy,
		selector: &lifecycleRequestSelector{
			ApplicantAgentID: legacy.ApplicantAgentID, RequestID: legacy.RequestID,
			LegacyAmbiguous: true,
		},
	}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})
	intent := validLifecycleIntent(legacy.PayloadDigest)
	intent.RequestID = legacy.RequestID

	receipt, replayed, err := store.ReserveLifecycleReceipt(context.Background(), intent)
	require.NoError(t, err)
	require.True(t, replayed)
	require.Equal(t, legacy.Action, receipt.Action)

	changed := intent
	changed.Action = LifecycleActionStop
	_, _, err = store.ReserveLifecycleReceipt(context.Background(), changed)
	require.ErrorIs(t, err, ErrLifecyclePayloadConflict)
	require.Equal(t, 0, tx.insertCalls, "legacy exact replay and cross-action conflict never insert a receipt")
}

func TestReserveLifecycleReceiptConvergesOnConcurrentSamePayload(t *testing.T) {
	t.Parallel()

	// A concurrent reserve already won the request-selector key with the same payload
	// (state still prepared). The second reserve's upsert no-ops, the current
	// read returns the winner's row, and the second reserve reports a replay —
	// both converge on one receipt.
	winner := &LifecycleReceipt{
		WorkspaceID: "wsp-1", ApplicantAgentID: "agt-caller", OwnerUserID: "usr-owner",
		Action: LifecycleActionStart, TargetAgentID: "agt-target", RequestID: "req-1",
		PayloadDigest: "digest-a", State: LifecycleReceiptStatePrepared, CorrelationID: "corr-1",
	}
	tx := &lifecycleFakeTx{row: winner}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	receipt, replayed, err := store.ReserveLifecycleReceipt(context.Background(), validLifecycleIntent("digest-a"))

	require.NoError(t, err)
	require.True(t, replayed, "a reserve that loses the selector race adopts the winner")
	require.Equal(t, "digest-a", receipt.PayloadDigest)
}

func TestReserveLifecycleReceiptPropagatesInsertError(t *testing.T) {
	t.Parallel()

	tx := &lifecycleFakeTx{insertErr: errors.New("disk full")}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	_, _, err := store.ReserveLifecycleReceipt(context.Background(), validLifecycleIntent("digest-a"))

	require.Error(t, err)
}

func TestReserveLifecycleReceiptRejectsInvalidAction(t *testing.T) {
	t.Parallel()

	store := New(&lifecycleFakeDB{tx: &lifecycleFakeTx{}}, Options{})
	intent := validLifecycleIntent("digest-a")
	intent.Action = "explode"

	_, _, err := store.ReserveLifecycleReceipt(context.Background(), intent)

	require.Error(t, err)
}

func TestMarkLifecycleDispatchingIssuesCASUpdate(t *testing.T) {
	t.Parallel()

	tx := &lifecycleFakeTx{}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	transitioned, err := store.MarkLifecycleDispatching(context.Background(), "agt-caller", LifecycleActionStart, "req-1")

	require.NoError(t, err)
	require.True(t, transitioned)
	require.Len(t, tx.updateCalls, 1)
	require.Equal(t, LifecycleReceiptStateDispatching, tx.updateCalls[0].state)
}

func TestMarkLifecycleDispatchingReportsAlreadyAdvanced(t *testing.T) {
	t.Parallel()

	tx := &lifecycleFakeTx{updateRowsAffected: lifecycleInt64(0)}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	transitioned, err := store.MarkLifecycleDispatching(context.Background(), "agt-caller", LifecycleActionStart, "req-1")

	require.NoError(t, err)
	require.False(t, transitioned, "a receipt no longer prepared does not transition")
}

func TestRecordLifecycleResultWritesTerminalState(t *testing.T) {
	t.Parallel()

	tx := &lifecycleFakeTx{updateRowsAffected: lifecycleInt64(1)}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	recorded, err := store.RecordLifecycleResult(context.Background(), "agt-caller", LifecycleActionRestart, "req-1",
		LifecycleReceiptStateDispatching, LifecycleReceiptStateIndeterminate, "runtime_outcome_unknown")

	require.NoError(t, err)
	require.True(t, recorded, "CAS from dispatching to terminal records the result")
	require.Len(t, tx.updateCalls, 1)
	require.Equal(t, LifecycleReceiptStateIndeterminate, tx.updateCalls[0].state)
	require.Equal(t, "runtime_outcome_unknown", tx.updateCalls[0].resultStatus)
}

func TestRecordLifecycleResultDoesNotOverwriteTerminal(t *testing.T) {
	t.Parallel()

	tx := &lifecycleFakeTx{updateRowsAffected: lifecycleInt64(0)}
	store := New(&lifecycleFakeDB{tx: tx}, Options{})

	recorded, err := store.RecordLifecycleResult(context.Background(), "agt-caller", LifecycleActionRestart, "req-1",
		LifecycleReceiptStateDispatching, LifecycleReceiptStateIndeterminate, "runtime_outcome_unknown")

	require.NoError(t, err)
	require.False(t, recorded, "a receipt no longer in the expected state is not overwritten")
}

func TestRecordLifecycleResultRejectsInvalidTransitions(t *testing.T) {
	t.Parallel()

	store := New(&lifecycleFakeDB{tx: &lifecycleFakeTx{}}, Options{})
	ctx := context.Background()

	_, err := store.RecordLifecycleResult(ctx, "agt-caller", LifecycleActionStart, "req-1",
		LifecycleReceiptStatePrepared, LifecycleReceiptStateSucceeded, "started")
	require.Error(t, err, "recording from a non-dispatching state is rejected")

	_, err = store.RecordLifecycleResult(ctx, "agt-caller", LifecycleActionStart, "req-1",
		LifecycleReceiptStateDispatching, LifecycleReceiptStateDispatching, "started")
	require.Error(t, err, "recording a non-terminal state is rejected")

	_, err = store.RecordLifecycleResult(ctx, "agt-caller", LifecycleActionStart, "req-1",
		LifecycleReceiptStateDispatching, LifecycleReceiptStateSucceeded, "  ")
	require.Error(t, err, "an empty result status is rejected")

	_, err = store.RecordLifecycleResult(ctx, "agt-caller", LifecycleActionRestart, "req-1",
		LifecycleReceiptStateDispatching, LifecycleReceiptStateSucceeded, "already_stopped")
	require.Error(t, err, "already_stopped is not a legal restart result status")

	_, err = store.RecordLifecycleResult(ctx, "agt-caller", LifecycleActionStart, "req-1",
		LifecycleReceiptStateDispatching, LifecycleReceiptStateSucceeded, "runtime_outcome_unknown")
	require.Error(t, err, "succeeded state must not pair with runtime_outcome_unknown")

	_, err = store.RecordLifecycleResult(ctx, "agt-caller", LifecycleActionStart, "req-1",
		LifecycleReceiptStateDispatching, LifecycleReceiptStateIndeterminate, "started")
	require.Error(t, err, "indeterminate state must not pair with a success status")
}
