//go:build integration

package workspace

import (
	"context"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/stretchr/testify/require"
	tcpg "github.com/testcontainers/testcontainers-go/modules/postgres"

	"github.com/yan1h/agent-coding-workflow/internal/infra/db"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// setupRepo 起一个临时 postgres 容器、跑迁移、注入一条 user/project 行做外键，
// 返回可直接调用的 Repository 与所属 project_id。每个测试用独立容器，互不干扰，
// 便于 Parallel。
func setupRepo(t *testing.T) (Repository, *pgxpool.Pool, uuid.UUID) {
	t.Helper()
	ctx := context.Background()
	container, err := tcpg.Run(ctx, "postgres:16-alpine",
		tcpg.WithDatabase("acw"),
		tcpg.WithUsername("acw"),
		tcpg.WithPassword("acw"),
	)
	require.NoError(t, err)
	t.Cleanup(func() { _ = container.Terminate(ctx) })

	dsn, err := container.ConnectionString(ctx, "sslmode=disable")
	require.NoError(t, err)

	require.NoError(t, db.Migrate(ctx, dsn, "file://../../migrations"))

	pool, err := db.NewPool(ctx, dsn)
	require.NoError(t, err)
	t.Cleanup(pool.Close)

	uid := uuid.New()
	_, err = pool.Exec(ctx, `INSERT INTO users (id,email,password_hash,display_name,is_admin,created_at,updated_at)
		VALUES ($1,'a@b.c','x','admin',true,now(),now())`, uid)
	require.NoError(t, err)
	pid := uuid.New()
	_, err = pool.Exec(ctx, `INSERT INTO projects (id,slug,name,owner_id,visibility) VALUES ($1,'p','P',$2,'private')`, pid, uid)
	require.NoError(t, err)
	return NewPostgresRepository(pool), pool, pid
}

func TestRepo_CreateWorkspaceAndRead(t *testing.T) {
	t.Parallel()
	repo, _, pid := setupRepo(t)
	ctx := context.Background()
	id := uuid.New()
	ws, err := repo.CreateWorkspace(ctx, &Workspace{
		ID: id, ProjectID: pid, Slug: "ws1", Name: "W1",
		GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/data/ws1/main",
		SyncStatus: SyncStatusCloning,
	})
	require.NoError(t, err)
	require.Equal(t, "ws1", ws.Slug)

	got, err := repo.GetWorkspaceByID(ctx, id)
	require.NoError(t, err)
	require.Equal(t, "W1", got.Name)
}

func TestRepo_SyncStatusRoundtrip(t *testing.T) {
	t.Parallel()
	repo, _, pid := setupRepo(t)
	ctx := context.Background()
	id := uuid.New()
	_, err := repo.CreateWorkspace(ctx, &Workspace{
		ID: id, ProjectID: pid, Slug: "ws2", Name: "W2",
		GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusCloning,
	})
	require.NoError(t, err)
	now := time.Now().UTC()
	_, err = repo.UpdateWorkspaceSyncStatus(ctx, id, SyncStatusError, &now, "boom")
	require.NoError(t, err)
	got, err := repo.GetWorkspaceByID(ctx, id)
	require.NoError(t, err)
	require.Equal(t, SyncStatusError, got.SyncStatus)
	require.Equal(t, "boom", got.LastSyncError)
}

func TestRepo_AcquireRelease_Concurrent(t *testing.T) {
	t.Parallel()
	repo, _, pid := setupRepo(t)
	ctx := context.Background()
	wsID := uuid.New()
	_, err := repo.CreateWorkspace(ctx, &Workspace{
		ID: wsID, ProjectID: pid, Slug: "ws3", Name: "W3",
		GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusIdle,
	})
	require.NoError(t, err)

	first := make(chan error, 1)
	second := make(chan error, 1)
	holderA, holderB := "user:a", "user:b"
	go func() {
		first <- repo.InTx(ctx, func(tx Tx) error {
			wt, err := tx.GetWorktreeForUpdate(ctx, wsID, "feat")
			if err == nil && wt.Status == WorktreeStatusActive {
				return nil
			}
			_, err = tx.InsertWorktree(ctx, &Worktree{
				ID: uuid.New(), WorkspaceID: wsID, Branch: "feat",
				Path: "/x/.worktrees/feat", Status: WorktreeStatusActive,
				ActiveHolder: holderA,
			})
			return err
		})
	}()
	<-first
	go func() {
		second <- repo.InTx(ctx, func(tx Tx) error {
			wt, err := tx.GetWorktreeForUpdate(ctx, wsID, "feat")
			if err != nil {
				return err
			}
			if wt.Status == WorktreeStatusActive && wt.ActiveHolder != holderB {
				return errsAlreadyActive()
			}
			return nil
		})
	}()
	require.Error(t, <-second)
}

func errsAlreadyActive() error {
	return &shimErr{}
}

type shimErr struct{}

func (e *shimErr) Error() string { return "already active" }

// TestRepo_SetWorktreeActive_ClearsPruneWarning 验证 SetWorktreeActive
// 会清空 prune_warning_at 列；同样验证 SetWorktreeIdle。前置：用直接 SQL
// 把列写成非 NULL；行为：repo 调用后再读 DB 应回到 NULL。
func TestRepo_SetWorktreeActive_ClearsPruneWarning(t *testing.T) {
	t.Parallel()
	repo, pool, pid := setupRepo(t)
	ctx := context.Background()
	wsID := uuid.New()
	_, err := repo.CreateWorkspace(ctx, &Workspace{
		ID: wsID, ProjectID: pid, Slug: "ws-pw", Name: "PW",
		GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusIdle,
	})
	require.NoError(t, err)

	wtID := uuid.New()
	require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
		_, e := tx.InsertWorktree(ctx, &Worktree{
			ID: wtID, WorkspaceID: wsID, Branch: "warn-branch",
			Path: "/x/.worktrees/warn", Status: WorktreeStatusIdle,
		})
		return e
	}))

	// 把 prune_warning_at 写成非 NULL。
	_, err = pool.Exec(ctx, `UPDATE workspace_worktrees SET prune_warning_at = now() WHERE id = $1`, wtID)
	require.NoError(t, err)

	var pwAt *time.Time
	require.NoError(t, pool.QueryRow(ctx, `SELECT prune_warning_at FROM workspace_worktrees WHERE id = $1`, wtID).Scan(&pwAt))
	require.NotNil(t, pwAt, "precondition: prune_warning_at should be set")

	// SetWorktreeActive 应该清空它。
	require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
		_, e := tx.SetWorktreeActive(ctx, wtID, "user:tester")
		return e
	}))
	require.NoError(t, pool.QueryRow(ctx, `SELECT prune_warning_at FROM workspace_worktrees WHERE id = $1`, wtID).Scan(&pwAt))
	require.Nil(t, pwAt, "SetWorktreeActive should clear prune_warning_at")

	// 再写回非 NULL，验证 SetWorktreeIdle 也会清空。
	_, err = pool.Exec(ctx, `UPDATE workspace_worktrees SET prune_warning_at = now() WHERE id = $1`, wtID)
	require.NoError(t, err)
	require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
		_, e := tx.SetWorktreeIdle(ctx, wtID)
		return e
	}))
	require.NoError(t, pool.QueryRow(ctx, `SELECT prune_warning_at FROM workspace_worktrees WHERE id = $1`, wtID).Scan(&pwAt))
	require.Nil(t, pwAt, "SetWorktreeIdle should clear prune_warning_at")
}

// TestRepo_TryStartPrune_CASLossReturnsAlreadyActive 验证：对一个已变为 active 的
// worktree 调用 TryStartPrune 会返回 CodeWorktreeAlreadyActive。
func TestRepo_TryStartPrune_CASLossReturnsAlreadyActive(t *testing.T) {
	t.Parallel()
	repo, pool, pid := setupRepo(t)
	ctx := context.Background()
	wsID := uuid.New()
	_, err := repo.CreateWorkspace(ctx, &Workspace{
		ID: wsID, ProjectID: pid, Slug: "ws-cas", Name: "CAS",
		GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusIdle,
	})
	require.NoError(t, err)

	wtID := uuid.New()
	require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
		_, e := tx.InsertWorktree(ctx, &Worktree{
			ID: wtID, WorkspaceID: wsID, Branch: "cas-branch",
			Path: "/x/.worktrees/cas", Status: WorktreeStatusIdle,
		})
		return e
	}))

	// 绕过 service 直接把 status 改成 active，模拟 CAS 竞争丢失。
	_, err = pool.Exec(ctx, `UPDATE workspace_worktrees SET status = 'active' WHERE id = $1`, wtID)
	require.NoError(t, err)

	_, err = repo.TryStartPrune(ctx, wtID)
	require.Error(t, err)
	appErr, ok := errs.As(err)
	require.True(t, ok, "expected AppError")
	require.Equal(t, errs.CodeWorktreeAlreadyActive, appErr.Code)
}

// TestRepo_ListWorktreesNeedingPruneWarning_FiltersExisting 验证：已有
// prune_warning_at 的 worktree 不会被返回，仅返回尚未标记的。
func TestRepo_ListWorktreesNeedingPruneWarning_FiltersExisting(t *testing.T) {
	t.Parallel()
	repo, pool, pid := setupRepo(t)
	ctx := context.Background()
	wsID := uuid.New()
	_, err := repo.CreateWorkspace(ctx, &Workspace{
		ID: wsID, ProjectID: pid, Slug: "ws-pwf", Name: "PWF",
		GitRemoteURL: "https://x", DefaultBranch: "main", MainPath: "/x", SyncStatus: SyncStatusIdle,
	})
	require.NoError(t, err)

	cutoff := time.Now().Add(-24 * time.Hour)

	// worktree A：没有 prune_warning_at，last_used_at 在 cutoff 之前 → 应返回。
	wtA := uuid.New()
	require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
		_, e := tx.InsertWorktree(ctx, &Worktree{
			ID: wtA, WorkspaceID: wsID, Branch: "prune-a",
			Path: "/x/.worktrees/a", Status: WorktreeStatusIdle,
		})
		return e
	}))
	_, err = pool.Exec(ctx, `UPDATE workspace_worktrees SET last_used_at = $1 WHERE id = $2`, cutoff.Add(-time.Hour), wtA)
	require.NoError(t, err)

	// worktree B：已经有 prune_warning_at，last_used_at 同样在 cutoff 前 → 不应返回。
	wtB := uuid.New()
	require.NoError(t, repo.InTx(ctx, func(tx Tx) error {
		_, e := tx.InsertWorktree(ctx, &Worktree{
			ID: wtB, WorkspaceID: wsID, Branch: "prune-b",
			Path: "/x/.worktrees/b", Status: WorktreeStatusIdle,
		})
		return e
	}))
	_, err = pool.Exec(ctx,
		`UPDATE workspace_worktrees SET last_used_at = $1, prune_warning_at = now() WHERE id = $2`,
		cutoff.Add(-time.Hour), wtB)
	require.NoError(t, err)

	rows, err := repo.ListWorktreesNeedingPruneWarning(ctx, cutoff)
	require.NoError(t, err)
	require.Len(t, rows, 1)
	require.Equal(t, wtA, rows[0].ID)
}
