package review

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"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/storeerr"
)

func taskReviewParams(taskScopeID, sourceAgentID, rootPath, displayName string) agent.UpsertReviewContextParams {
	return agent.UpsertReviewContextParams{
		WorkspaceID:   "ws_task_review_persistence",
		TaskScopeID:   taskScopeID,
		SourceAgentID: sourceAgentID,
		MachineID:     "mac_task_review",
		RootPath:      rootPath,
		BaseRef:       "main",
		HeadRef:       "feature",
		DisplayName:   displayName,
		PRURL:         "https://reviews.example/initial",
	}
}

// TestReviewContextTaskSlotPersistenceContracts exercises the real TiDB
// unique indexes through the same transaction path used by Agent Runtime.
// It deliberately calls the store's validated create boundary directly so the
// test can isolate persistence semantics from Task/daemon authorization setup.
func TestReviewContextTaskSlotPersistenceContracts(t *testing.T) {
	ctx := context.Background()
	db := startReviewRollbackDB(t, ctx)
	store := newReviewStoreWithPermissiveAccess(db)
	const (
		taskScopeID = "tsk_review_persistence"
		sourceAgent = "agt_review_persistence"
	)

	// The atomic SQL test still enters through the production project resolver
	// so both replacement targets have real, distinct Project rows. The direct
	// createReviewContext call is intentional here: Task authorization is
	// covered by the handler/API tests, while this test isolates the transaction
	// boundary and can assert every authoritative column, including project_id.
	projectA, err := store.GetOrCreateProjectForUser(ctx, "usr_task_review_project_owner", agent.GetOrCreateProjectParams{
		WorkspaceID: "ws_task_review_persistence", MachineID: "mac_task_review", RootPath: "/repo/task-review/one", Name: "Project A",
	})
	require.NoError(t, err)
	projectB, err := store.GetOrCreateProjectForUser(ctx, "usr_task_review_project_owner", agent.GetOrCreateProjectParams{
		WorkspaceID: "ws_task_review_persistence", MachineID: "mac_task_review_replaced", RootPath: "/repo/task-review/two", Name: "Project B",
	})
	require.NoError(t, err)
	require.NotEqual(t, projectA.ID, projectB.ID)

	firstParams := taskReviewParams(taskScopeID, sourceAgent, "/repo/task-review/one", "first")
	firstParams.ProjectID = projectA.ID
	firstParams.MachineID = "mac_task_review"
	firstParams.BaseRef = "main"
	firstParams.HeadRef = "feature-a"
	first, err := store.createReviewContext(ctx, firstParams)
	require.NoError(t, err)
	require.NotEmpty(t, first.ID)
	require.Equal(t, taskScopeID, first.TaskScopeID)
	require.Equal(t, sourceAgent, first.SourceAgentID)
	require.Equal(t, projectA.ID, first.ProjectID)

	// Same target is a metadata refresh, not a new row. The stable context ID
	// and authoritative target tuple survive the refresh.
	refreshParams := firstParams
	refreshParams.DisplayName = "refreshed"
	refreshParams.PRURL = "https://reviews.example/refreshed"
	refreshed, err := store.createReviewContext(ctx, refreshParams)
	require.NoError(t, err)
	require.Equal(t, first.ID, refreshed.ID)
	require.Equal(t, "refreshed", refreshed.DisplayName)
	require.Equal(t, "https://reviews.example/refreshed", refreshed.PRURL)
	require.Equal(t, projectA.ID, refreshed.ProjectID)
	require.Equal(t, "/repo/task-review/one", refreshed.RootPath)

	// Replacing the Task/source slot writes the complete target tuple in one
	// transaction while preserving the context identity. Project B is a valid
	// row resolved for the replacement machine/root target, so project_id is
	// part of the same authoritative replacement assertion.
	replaceParams := taskReviewParams(taskScopeID, sourceAgent, "/repo/task-review/two", "replaced")
	replaceParams.ProjectID = projectB.ID
	replaceParams.MachineID = "mac_task_review_replaced"
	replaceParams.BaseRef = "trunk"
	replaceParams.HeadRef = "feature-b"
	replaceParams.PRURL = "https://reviews.example/replaced"
	replaced, err := store.createReviewContext(ctx, replaceParams)
	require.NoError(t, err)
	require.Equal(t, first.ID, replaced.ID)
	require.Equal(t, "/repo/task-review/two", replaced.RootPath)
	require.Equal(t, projectB.ID, replaced.ProjectID)
	require.Equal(t, reviewContextTaskTargetHash(taskScopeID, "mac_task_review_replaced", "/repo/task-review/two", "trunk", "feature-b"), persistedTaskTargetHash(t, ctx, db, taskScopeID, sourceAgent))
	var persisted struct {
		TargetHash string
		MachineID  string
		RootPath   string
		BaseRef    string
		HeadRef    string
		Display    string
		PRURL      string
		ProjectID  string
	}
	require.NoError(t, db.QueryRow(ctx, `
SELECT target_hash, machine_id, root_path, base_ref, head_ref, display_name, pr_url, project_id
FROM review_contexts
WHERE workspace_id = $1 AND task_scope_id = $2 AND source_agent_id = $3
`, "ws_task_review_persistence", taskScopeID, sourceAgent).Scan(
		&persisted.TargetHash, &persisted.MachineID, &persisted.RootPath, &persisted.BaseRef,
		&persisted.HeadRef, &persisted.Display, &persisted.PRURL, &persisted.ProjectID,
	))
	require.Equal(t, replaceParams.MachineID, persisted.MachineID)
	require.Equal(t, replaceParams.RootPath, persisted.RootPath)
	require.Equal(t, replaceParams.BaseRef, persisted.BaseRef)
	require.Equal(t, replaceParams.HeadRef, persisted.HeadRef)
	require.Equal(t, replaceParams.DisplayName, persisted.Display)
	require.Equal(t, replaceParams.PRURL, persisted.PRURL)
	require.Equal(t, projectB.ID, persisted.ProjectID)

	var oldTargetCount int
	require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM review_contexts WHERE workspace_id = $1 AND task_scope_id = $2 AND target_hash = $3`,
		"ws_task_review_persistence", taskScopeID,
		reviewContextTaskTargetHash(taskScopeID, "mac_task_review", "/repo/task-review/one", "main", "feature-a"),
	).Scan(&oldTargetCount))
	require.Zero(t, oldTargetCount, "replacement must remove the old target identity")

	// A different source agent may own the desired target, but the first slot
	// cannot steal it. Both rows must remain byte-for-byte authoritative after
	// the typed conflict.
	other, err := store.createReviewContext(ctx, taskReviewParams(taskScopeID, "agt_other", "/repo/task-review/three", "other"))
	require.NoError(t, err)
	_, err = store.createReviewContext(ctx, taskReviewParams(taskScopeID, sourceAgent, "/repo/task-review/three", "conflict"))
	require.ErrorIs(t, err, storeerr.ErrConflict)
	require.Equal(t, "/repo/task-review/two", loadTaskReviewRoot(t, ctx, db, taskScopeID, sourceAgent))
	require.Equal(t, other.ID, loadTaskReviewID(t, ctx, db, taskScopeID, "agt_other"))

	// The same rejected request is deterministic and idempotent: retrying it
	// reports the same conflict and does not mutate either slot.
	_, retryErr := store.createReviewContext(ctx, taskReviewParams(taskScopeID, sourceAgent, "/repo/task-review/three", "conflict-retry"))
	require.ErrorIs(t, retryErr, storeerr.ErrConflict)
	require.Equal(t, "/repo/task-review/two", loadTaskReviewRoot(t, ctx, db, taskScopeID, sourceAgent))

	// Empty/non-canonical Task scopes fail before any row is enumerated or
	// written, as required by the public Task contract.
	_, err = store.createReviewContext(ctx, taskReviewParams("scope_not_task", sourceAgent, "/repo/task-review/four", "invalid"))
	require.Error(t, err)
	require.False(t, errors.Is(err, storeerr.ErrConflict))
}

func persistedTaskTargetHash(t *testing.T, ctx context.Context, db coredb.QueryRower, taskScopeID, sourceAgentID string) string {
	t.Helper()
	var targetHash string
	if err := db.QueryRow(ctx, `SELECT target_hash FROM review_contexts WHERE workspace_id = $1 AND task_scope_id = $2 AND source_agent_id = $3`,
		"ws_task_review_persistence", taskScopeID, sourceAgentID).Scan(&targetHash); err != nil {
		t.Fatalf("load persisted task target hash: %v", err)
	}
	return targetHash
}

func loadTaskReviewRoot(t *testing.T, ctx context.Context, db coredb.QueryRower, taskScopeID, sourceAgentID string) string {
	t.Helper()
	var rootPath string
	if err := db.QueryRow(ctx, `SELECT root_path FROM review_contexts WHERE workspace_id = $1 AND task_scope_id = $2 AND source_agent_id = $3`,
		"ws_task_review_persistence", taskScopeID, sourceAgentID).Scan(&rootPath); err != nil {
		t.Fatalf("load persisted task root: %v", err)
	}
	return rootPath
}

func loadTaskReviewID(t *testing.T, ctx context.Context, db coredb.QueryRower, taskScopeID, sourceAgentID string) string {
	t.Helper()
	var id string
	if err := db.QueryRow(ctx, `SELECT id FROM review_contexts WHERE workspace_id = $1 AND task_scope_id = $2 AND source_agent_id = $3`,
		"ws_task_review_persistence", taskScopeID, sourceAgentID).Scan(&id); err != nil {
		t.Fatalf("load persisted task context id: %v", err)
	}
	return id
}

// TestReviewContextTaskSlotConcurrentReplacement uses independent TiDB
// connections and committed transactions. The final winner is intentionally
// unspecified; the load-bearing guarantees are one stable context ID, one
// row, and a complete target tuple from one of the serialized replacements.
func TestReviewContextTaskSlotConcurrentReplacement(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer cancel()
	db := startFreshIsolatedDB(t, ctx)
	store := newReviewStoreWithPermissiveAccess(db)
	initial, err := store.createReviewContext(ctx, taskReviewParams("tsk_task_concurrent", "agt_task_concurrent", "/repo/task-concurrent/initial", "initial"))
	require.NoError(t, err)

	const concurrency = 8
	roots := make([]string, concurrency)
	for i := range roots {
		roots[i] = fmt.Sprintf("/repo/task-concurrent/replacement-%c", 'a'+i)
	}
	barrier := make(chan struct{})
	results := make([]agent.ReviewContext, concurrency)
	errs := make([]error, concurrency)
	var wg sync.WaitGroup
	for i := 0; i < concurrency; i++ {
		wg.Add(1)
		go func(index int) {
			defer wg.Done()
			<-barrier
			results[index], errs[index] = store.createReviewContext(ctx, taskReviewParams("tsk_task_concurrent", "agt_task_concurrent", roots[index], "replacement"))
		}(i)
	}
	close(barrier)
	wg.Wait()

	for i, err := range errs {
		require.NoError(t, err, "concurrent replacement %d", i)
		require.Equal(t, initial.ID, results[i].ID, "replacement %d minted a second context", i)
	}
	var count int
	require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM review_contexts WHERE workspace_id = $1 AND task_scope_id = $2 AND source_agent_id = $3`,
		"ws_task_review_persistence", "tsk_task_concurrent", "agt_task_concurrent").Scan(&count))
	require.Equal(t, 1, count)
	var finalRoot string
	require.NoError(t, db.QueryRow(ctx, `SELECT root_path FROM review_contexts WHERE workspace_id = $1 AND task_scope_id = $2 AND source_agent_id = $3`,
		"ws_task_review_persistence", "tsk_task_concurrent", "agt_task_concurrent").Scan(&finalRoot))
	require.Contains(t, roots, finalRoot)
}
