package agentops

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/onboarding"
)

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

type agentOpsFakeDB struct {
	tx *agentOpsFakeTx
}

func (db *agentOpsFakeDB) QueryRow(context.Context, string, ...any) coredb.Row {
	return agentOpsFakeRow{err: errAgentOpsFakeUnexpected}
}

func (db *agentOpsFakeDB) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return nil, errAgentOpsFakeUnexpected
}

func (db *agentOpsFakeDB) Exec(context.Context, string, ...any) (coredb.Result, error) {
	return nil, errAgentOpsFakeUnexpected
}

func (db *agentOpsFakeDB) BeginTx(context.Context, coredb.TxOptions) (coredb.Tx, error) {
	if db.tx == nil {
		db.tx = &agentOpsFakeTx{}
	}
	db.tx.ops = append(db.tx.ops, "begin")
	return db.tx, nil
}

type agentOpsFakeTx struct {
	ops []string
}

func (tx *agentOpsFakeTx) QueryRow(context.Context, string, ...any) coredb.Row {
	return agentOpsFakeRow{err: sql.ErrNoRows}
}

func (tx *agentOpsFakeTx) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return agentOpsFakeRows{}, nil
}

func (tx *agentOpsFakeTx) Exec(_ context.Context, query string, _ ...any) (coredb.Result, error) {
	if strings.Contains(query, "agent_runtime_failure_summaries") {
		tx.ops = append(tx.ops, "exec:agent_runtime_failure_summaries")
		return agentOpsFakeResult(1), nil
	}
	if strings.Contains(query, "agent_runtime_events") {
		tx.ops = append(tx.ops, "exec:agent_runtime_events")
		return agentOpsFakeResult(1), nil
	}
	tx.ops = append(tx.ops, "exec:other")
	return agentOpsFakeResult(1), nil
}

func (tx *agentOpsFakeTx) Select(_ context.Context, _ any, query string, _ ...any) error {
	if strings.Contains(query, "agent_runtime_failure_summaries") {
		tx.ops = append(tx.ops, "select:agent_runtime_failure_summaries")
		return nil
	}
	if strings.Contains(query, "agent_runtime_events") {
		tx.ops = append(tx.ops, "select:agent_runtime_events")
		return nil
	}
	tx.ops = append(tx.ops, "select:other")
	return nil
}

func (tx *agentOpsFakeTx) Commit(context.Context) error {
	tx.ops = append(tx.ops, "commit")
	return nil
}

func (tx *agentOpsFakeTx) Rollback(context.Context) error {
	tx.ops = append(tx.ops, "rollback")
	return nil
}

type agentOpsFakeRow struct {
	err error
}

func (row agentOpsFakeRow) Scan(...any) error {
	return row.err
}

type agentOpsFakeRows struct{}

func (agentOpsFakeRows) Close()            {}
func (agentOpsFakeRows) Err() error        { return nil }
func (agentOpsFakeRows) Next() bool        { return false }
func (agentOpsFakeRows) Scan(...any) error { return nil }

type agentOpsFakeResult int64

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

type failingOnboardingEvidenceRecorder struct{}

func (failingOnboardingEvidenceRecorder) RecordVisibleReply(context.Context, onboarding.VisibleReplyEvidence) error {
	return errors.New("projection unavailable")
}

func validAgentRuntimeEvent() agent.AgentRuntimeEvent {
	return agent.AgentRuntimeEvent{
		WorkspaceID: "wsp-1",
		AgentID:     "agt-1",
		MachineID:   "mch-1",
		MessageID:   "msg-1",
		EventClass:  AgentRuntimeEventClassRuntimeStartFailed,
		OccurredAt:  time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
	}
}

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

	tx := &agentOpsFakeTx{}
	store := New(&agentOpsFakeDB{}, Options{})

	err := store.InTx(tx).RecordAgentRuntimeEvent(context.Background(), validAgentRuntimeEvent())

	require.NoError(t, err)
	require.Equal(t, []string{"exec:agent_runtime_events", "exec:agent_runtime_failure_summaries"}, tx.ops)
}

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

	tests := []string{
		AgentRuntimeEventClassVisibleReplySent,
		AgentRuntimeEventClassRuntimeRetrying,
	}

	for _, eventClass := range tests {
		t.Run(eventClass, func(t *testing.T) {
			t.Parallel()

			tx := &agentOpsFakeTx{}
			store := New(&agentOpsFakeDB{}, Options{})
			event := validAgentRuntimeEvent()
			event.EventClass = eventClass

			err := store.InTx(tx).RecordAgentRuntimeEvent(context.Background(), event)

			require.NoError(t, err)
			require.Equal(t, []string{"exec:agent_runtime_events"}, tx.ops)
		})
	}
}

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

	db := &agentOpsFakeDB{tx: &agentOpsFakeTx{}}
	store := New(db, Options{OnboardingEvidence: failingOnboardingEvidenceRecorder{}})
	event := validAgentRuntimeEvent()
	event.EventClass = AgentRuntimeEventClassVisibleReplySent
	event.ReplyMessageID = "reply-1"

	err := store.RecordAgentRuntimeEvent(context.Background(), event)

	require.NoError(t, err)
	require.Equal(t, []string{"begin", "exec:agent_runtime_events", "commit"}, db.tx.ops)
}

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

	ctx := context.Background()
	tests := []struct {
		name string
		run  func(*Store) error
	}{
		{
			name: "latest failure summary",
			run: func(store *Store) error {
				_, err := store.LoadLatestAgentRuntimeFailureSummary(ctx, "wsp-1", "agt-1")
				return err
			},
		},
		{
			name: "latest failure summaries",
			run: func(store *Store) error {
				_, err := store.LoadLatestAgentRuntimeFailureSummaries(ctx, "wsp-1", []string{"agt-1"})
				return err
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			db := &agentOpsFakeDB{tx: &agentOpsFakeTx{}}
			store := New(db, Options{})

			err := tt.run(store)

			require.NoError(t, err)
			expectedOps := []string{"begin", "commit"}
			switch tt.name {
			case "delivery evidence":
				expectedOps = []string{"begin", "select:agent_runtime_events", "commit"}
			case "latest failure summary", "latest failure summaries":
				expectedOps = []string{"begin", "select:agent_runtime_failure_summaries", "commit"}
			}
			require.Equal(t, expectedOps, db.tx.ops)
		})
	}
}

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

	db := &agentOpsFakeDB{tx: &agentOpsFakeTx{}}
	store := New(db, Options{})

	err := store.RecordAgentRuntimeEvent(context.Background(), agent.AgentRuntimeEvent{})

	require.Error(t, err)
	require.Equal(t, []string{"begin", "rollback"}, db.tx.ops)
}

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

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

	_, err := store.InTx(tx).LoadLatestAgentRuntimeFailureSummaries(ctx, "wsp-1", []string{"agt-1"})
	require.NoError(t, err)

	require.Equal(t, []string{"select:agent_runtime_failure_summaries"}, tx.ops)
}

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

	query, args, err := buildLoadLatestAgentRuntimeFailureSummariesQuery(
		"wsp-1",
		[]string{"agt-1", "agt-2"},
	)
	require.NoError(t, err)
	require.Equal(t, []any{
		"wsp-1",
		"agt-1",
		"agt-2",
	}, args)
	require.Contains(t, query, "WITH agent_ids AS (")
	require.Contains(t, query, "JOIN agent_runtime_failure_summaries summaries")
	require.Contains(t, query, "summaries.workspace_id = $1")
	require.NotContains(t, query, "event_classes AS (")
	require.NotContains(t, query, "ROW_NUMBER() OVER (")
	require.NotContains(t, query, "WHERE ranked_events.row_num = 1")
}
