package app

import (
	"bytes"
	"context"
	"database/sql"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/go-sql-driver/mysql"
	"github.com/stretchr/testify/require"

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

type stubRuntimeDB struct {
	queryRows func(context.Context, string, ...any) coredb.Row
	exec      func(context.Context, string, ...any) (coredb.Result, error)
	beginTx   func(context.Context, coredb.TxOptions) (coredb.Tx, error)
}

func (db *stubRuntimeDB) QueryRow(ctx context.Context, query string, args ...any) coredb.Row {
	if db.queryRows == nil {
		return stubScanRow{err: sql.ErrNoRows}
	}
	return db.queryRows(ctx, query, args...)
}

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

func (db *stubRuntimeDB) Exec(ctx context.Context, query string, args ...any) (coredb.Result, error) {
	if db.exec == nil {
		return testExecResult{}, nil
	}
	return db.exec(ctx, query, args...)
}

func (db *stubRuntimeDB) BeginTx(ctx context.Context, options coredb.TxOptions) (coredb.Tx, error) {
	if db.beginTx != nil {
		return db.beginTx(ctx, options)
	}
	return nil, nil
}

type stubRuntimeTx struct {
	queryRows func(context.Context, string, ...any) coredb.Row
	query     func(context.Context, string, ...any) (coredb.Rows, error)
	exec      func(context.Context, string, ...any) (coredb.Result, error)
}

func (tx *stubRuntimeTx) QueryRow(ctx context.Context, query string, args ...any) coredb.Row {
	if tx.queryRows == nil {
		return stubScanRow{err: sql.ErrNoRows}
	}
	return tx.queryRows(ctx, query, args...)
}

func (tx *stubRuntimeTx) Query(ctx context.Context, query string, args ...any) (coredb.Rows, error) {
	if tx.query != nil {
		return tx.query(ctx, query, args...)
	}
	return nil, nil
}

func (tx *stubRuntimeTx) Exec(ctx context.Context, query string, args ...any) (coredb.Result, error) {
	if tx.exec == nil {
		return testExecResult{}, nil
	}
	return tx.exec(ctx, query, args...)
}

func (tx *stubRuntimeTx) Commit(context.Context) error {
	return nil
}

func (tx *stubRuntimeTx) Rollback(context.Context) error {
	return nil
}

func (tx *stubRuntimeTx) Select(context.Context, any, string, ...any) error {
	return nil
}

type stubScanRow struct {
	values []any
	err    error
}

func (row stubScanRow) Scan(dest ...any) error {
	if row.err != nil {
		return row.err
	}
	if len(dest) != len(row.values) {
		return sql.ErrNoRows
	}
	for i := range dest {
		value := reflect.ValueOf(dest[i])
		if value.Kind() != reflect.Ptr || value.IsNil() {
			return sql.ErrNoRows
		}
		value.Elem().Set(reflect.ValueOf(row.values[i]))
	}
	return nil
}

type testExecResult struct {
	rowsAffected int64
}

func (result testExecResult) RowsAffected() int64 {
	return result.rowsAffected
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Unix(100, 0).UTC()
	fixture := seedAppAdminFixture(t, pool, "delete_machine_revoke", now)
	machineID := "mac_delete_revoke"
	apiKey := machineID + "-key"
	seedMachineForRuntimeList(t, pool, fixture.WorkspaceID, fixture.UserID, machineID, now)
	mustExec(t, pool, `
UPDATE machines
SET connected_at = $2, disconnected_at = NULL
WHERE id = $1
`, machineID, now)
	mustExec(t, pool, `
INSERT INTO machine_runtime_capabilities (machine_id, runtime, default_model, default_reasoning_effort)
VALUES ($1, 'codex', 'gpt-5-codex', 'medium')
`, machineID)

	store := New(pool, Options{})
	authStore := agent.New(pool)
	_, err := authStore.AuthenticateMachine(ctx, apiKey)
	require.NoError(t, err)

	require.NoError(t, store.DeleteMachineForWorkspace(ctx, fixture.UserID, fixture.WorkspaceID, machineID))

	_, err = authStore.AuthenticateMachine(ctx, apiKey)
	require.ErrorIs(t, err, agent.ErrUnauthorized)
	err = authStore.MarkMachineReady(ctx, agent.MachineIdentity{MachineID: machineID, WorkspaceID: fixture.WorkspaceID}, agent.ReadyPayload{Hostname: "stale-host"})
	require.ErrorIs(t, err, agent.ErrUnauthorized)

	machines, err := store.ListOwnMachinesForWorkspace(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.Empty(t, machines)

	var status string
	var storedAPIKey string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT status, api_key
FROM machines
WHERE id = $1
`, machineID).Scan(&status, &storedAPIKey))
	require.Equal(t, "revoked", status)
	require.NotEqual(t, apiKey, storedAPIKey)

	var runtimeCount int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM machine_runtime_capabilities
WHERE machine_id = $1
`, machineID).Scan(&runtimeCount))
	require.EqualValues(t, 0, runtimeCount)
}

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

	const (
		userID    = "usr_123"
		workspace = "ws_123"
		machineID = "mac_123"
	)

	db := &stubRuntimeDB{
		queryRows: func(ctx context.Context, query string, args ...any) coredb.Row {
			switch {
			case strings.Contains(query, "FROM members"):
				return stubScanRow{values: []any{"mem_123", "Alice", userID, "member"}}
			case strings.Contains(query, "FROM machines") && strings.Contains(query, "created_by_user_id"):
				return stubScanRow{values: []any{userID}}
			case strings.Contains(query, "FROM machines"):
				return stubScanRow{values: []any{machineID}}
			default:
				return stubScanRow{err: sql.ErrNoRows}
			}
		},
		beginTx: func(ctx context.Context, options coredb.TxOptions) (coredb.Tx, error) {
			return &stubRuntimeTx{
				exec: func(ctx context.Context, query string, args ...any) (coredb.Result, error) {
					return testExecResult{}, &mysql.MySQLError{
						Number:  1451,
						Message: "Cannot delete or update a parent row: a foreign key constraint fails",
					}
				},
			}, nil
		},
	}

	store := New(db, Options{})

	err := store.DeleteMachineForWorkspace(context.Background(), userID, workspace, machineID)

	require.ErrorIs(t, err, ErrConflict)
}

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

	writeKey := bytes.Repeat([]byte{1}, 32)
	readKey := bytes.Repeat([]byte{2}, 32)
	ciphertext, err := agent.EncryptAgentCredential(writeKey, "secret")
	require.NoError(t, err)

	store := New(nil, Options{AppCredentialStorageKey: readKey})
	appAgent := agent.AppAgent{ID: "agt_1"}

	store.applyAppAgentLLMViewFields(
		&appAgent,
		agent.AgentLLMModeLocal,
		"https://llm.example.com",
		ciphertext,
		[]byte(`{"X-Test":"yes"}`),
		"openai",
	)

	require.Equal(t, agent.AgentLLMModeLocal, appAgent.LLMMode)
	require.Equal(t, "https://llm.example.com", appAgent.BaseURL)
	require.Equal(t, map[string]string{"X-Test": "yes"}, appAgent.Headers)
	require.Equal(t, "openai", appAgent.WireAPI)
	require.True(t, appAgent.APIKeyConfigured)
	require.Equal(t, agent.AgentLLMConfigErrorUnreadable, appAgent.LLMConfigError)
}

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

	writeKey := bytes.Repeat([]byte{1}, 32)
	readKey := bytes.Repeat([]byte{2}, 32)
	ciphertext, err := agent.EncryptAgentCredential(writeKey, "secret")
	require.NoError(t, err)

	store := New(nil, Options{AppCredentialStorageKey: readKey})
	agentView := agent.AgentView{AgentID: "agt_1"}

	store.applyAgentViewLLMFields(
		&agentView,
		agent.AgentLLMModeLocal,
		"https://llm.example.com",
		ciphertext,
		[]byte(`{"X-Test":"yes"}`),
		"openai",
	)

	require.Equal(t, agent.AgentLLMModeLocal, agentView.LLMMode)
	require.Equal(t, "https://llm.example.com", agentView.BaseURL)
	require.Empty(t, agentView.APIKey)
	require.Equal(t, map[string]string{"X-Test": "yes"}, agentView.Headers)
	require.Equal(t, "openai", agentView.WireAPI)
	require.Equal(t, agent.AgentLLMConfigErrorUnreadable, agentView.LLMConfigError)
}

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

	const (
		userID      = "usr_1"
		workspaceID = "ws_1"
		agentID     = "agt_1"
		memberID    = "mem_agent"
		machineID   = "mac_1"
	)

	writeKey := bytes.Repeat([]byte{1}, 32)
	readKey := bytes.Repeat([]byte{2}, 32)
	ciphertext, err := agent.EncryptAgentCredential(writeKey, "secret")
	require.NoError(t, err)

	var updateQuery string
	var updateArgs []any
	var transactionOrder []string
	tx := &stubRuntimeTx{
		queryRows: func(_ context.Context, query string, _ ...any) coredb.Row {
			if strings.Contains(query, "SELECT id\nFROM agents") && strings.Contains(query, "FOR UPDATE") {
				transactionOrder = append(transactionOrder, "agent-lock")
				return stubScanRow{values: []any{agentID}}
			}
			if strings.Contains(query, "SELECT a.id, a.member_id") {
				transactionOrder = append(transactionOrder, "agent-view")
				return stubScanRow{values: []any{
					agentID,
					memberID,
					"Agent One",
					"description",
					"codex",
					agent.AgentPermissionModeFullAccess,
					"gpt-5.4",
					"medium",
					agent.AgentLLMModeLocal,
					"https://llm.example.com",
					ciphertext,
					[]byte(`{}`),
					"openai",
					machineID,
					"",
					"manual",
					"",
					true,
					"inactive",
					"offline",
					"",
					"",
				}}
			}
			return stubScanRow{err: sql.ErrNoRows}
		},
		exec: func(_ context.Context, query string, args ...any) (coredb.Result, error) {
			if strings.Contains(query, "UPDATE members") {
				transactionOrder = append(transactionOrder, "member-update")
			}
			if strings.Contains(query, "UPDATE agents") {
				transactionOrder = append(transactionOrder, "agent-update")
				updateQuery = query
				updateArgs = append([]any(nil), args...)
			}
			return testExecResult{rowsAffected: 1}, nil
		},
	}

	db := &stubRuntimeDB{
		queryRows: func(_ context.Context, query string, _ ...any) coredb.Row {
			switch {
			case strings.Contains(query, "FROM members"):
				return stubScanRow{values: []any{"mem_human", "Alice", userID, "member"}}
			case strings.Contains(query, "SELECT a.id, a.machine_id, a.member_id"):
				return stubScanRow{values: []any{agentID, machineID, memberID}}
			case strings.Contains(query, "JOIN machines"):
				return stubScanRow{values: []any{userID}}
			default:
				return stubScanRow{err: sql.ErrNoRows}
			}
		},
		beginTx: func(context.Context, coredb.TxOptions) (coredb.Tx, error) {
			return tx, nil
		},
	}
	store := New(db, Options{AppCredentialStorageKey: readKey})
	nextDisplayName := "Agent Renamed"

	_, err = store.UpdateAgentForUser(context.Background(), userID, workspaceID, agentID, agent.UpdateAgentParams{
		DisplayName: &nextDisplayName,
	})

	require.NoError(t, err)
	require.Contains(t, updateQuery, "llm_api_key_ciphertext = CASE WHEN $16 THEN $9 ELSE llm_api_key_ciphertext END")
	require.Len(t, updateArgs, 18)
	require.False(t, updateArgs[14].(bool))
	require.False(t, updateArgs[15].(bool))
	require.False(t, updateArgs[16].(bool))
	require.False(t, updateArgs[17].(bool))
	require.Equal(t, "agent-lock", transactionOrder[0])
	require.Less(t, indexOfRuntimeCall(transactionOrder, "agent-lock"), indexOfRuntimeCall(transactionOrder, "member-update"))
}

func indexOfRuntimeCall(calls []string, expected string) int {
	for index, call := range calls {
		if call == expected {
			return index
		}
	}
	return -1
}
