package workspace

import (
	"context"
	"errors"
	"log/slog"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/infra/git"
)

type fakeRepo struct {
	mu    sync.Mutex
	wss   map[uuid.UUID]*Workspace
	creds map[uuid.UUID]*Credential
	wts   map[uuid.UUID]*Worktree
	errOn map[string]error
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{
		wss: map[uuid.UUID]*Workspace{}, creds: map[uuid.UUID]*Credential{},
		wts: map[uuid.UUID]*Worktree{}, errOn: map[string]error{},
	}
}

func (f *fakeRepo) CreateWorkspace(_ context.Context, w *Workspace) (*Workspace, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if e := f.errOn["create"]; e != nil {
		return nil, e
	}
	cp := *w
	f.wss[w.ID] = &cp
	return &cp, nil
}
func (f *fakeRepo) GetWorkspaceByID(_ context.Context, id uuid.UUID) (*Workspace, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if w, ok := f.wss[id]; ok {
		cp := *w
		return &cp, nil
	}
	return nil, errors.New("not found")
}
func (f *fakeRepo) GetWorkspaceBySlug(_ context.Context, _, _ string) (*Workspace, error) {
	return nil, nil
}
func (f *fakeRepo) ListWorkspacesByProject(_ context.Context, _ uuid.UUID) ([]*Workspace, error) {
	out := make([]*Workspace, 0, len(f.wss))
	for _, w := range f.wss {
		out = append(out, w)
	}
	return out, nil
}
func (f *fakeRepo) UpdateWorkspaceCore(_ context.Context, id uuid.UUID, name, desc, br string) (*Workspace, error) {
	w := f.wss[id]
	w.Name, w.Description, w.DefaultBranch = name, desc, br
	return w, nil
}
func (f *fakeRepo) UpdateWorkspaceSyncStatus(_ context.Context, id uuid.UUID, st SyncStatus, _ *time.Time, msg string) (*Workspace, error) {
	w := f.wss[id]
	w.SyncStatus, w.LastSyncError = st, msg
	return w, nil
}
func (f *fakeRepo) DeleteWorkspace(_ context.Context, id uuid.UUID) error {
	delete(f.wss, id)
	return nil
}
func (f *fakeRepo) ResetStuckSyncStatuses(_ context.Context) ([]uuid.UUID, error) {
	return nil, nil
}
func (f *fakeRepo) ListFetchTargets(_ context.Context) ([]FetchTarget, error) { return nil, nil }

// MarkSyncing 模拟真实实现的 CAS 语义：仅 idle/error 可抢到锁并置为 syncing。
func (f *fakeRepo) MarkSyncing(_ context.Context, id uuid.UUID) (bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	w, ok := f.wss[id]
	if !ok {
		return false, errors.New("not found")
	}
	if w.SyncStatus != SyncStatusIdle && w.SyncStatus != SyncStatusError {
		return false, nil
	}
	w.SyncStatus = SyncStatusSyncing
	return true, nil
}
func (f *fakeRepo) MarkFetchResult(_ context.Context, _ uuid.UUID, _ bool, _ string) error {
	return nil
}
func (f *fakeRepo) GetWorktreeByID(_ context.Context, id uuid.UUID) (*Worktree, error) {
	if w, ok := f.wts[id]; ok {
		return w, nil
	}
	return nil, errors.New("not found")
}
func (f *fakeRepo) ListWorktreesByWorkspace(_ context.Context, _ uuid.UUID) ([]*Worktree, error) {
	return nil, nil
}
func (f *fakeRepo) DeleteWorktree(_ context.Context, _ uuid.UUID) error { return nil }
func (f *fakeRepo) ListWorktreesNeedingPruneWarning(_ context.Context, _ time.Time) ([]*Worktree, error) {
	return nil, nil
}
func (f *fakeRepo) ListWorktreesNeedingPrune(_ context.Context, _ time.Time) ([]*Worktree, error) {
	return nil, nil
}
func (f *fakeRepo) MarkPruneWarning(_ context.Context, _ uuid.UUID) error { return nil }
func (f *fakeRepo) TryStartPrune(_ context.Context, _ uuid.UUID) (*Worktree, error) {
	return nil, nil
}
func (f *fakeRepo) FinishPruneSuccess(_ context.Context, _ uuid.UUID) error  { return nil }
func (f *fakeRepo) FinishPruneRollback(_ context.Context, _ uuid.UUID) error { return nil }
func (f *fakeRepo) ReleaseWorktreesByHolder(_ context.Context, _ string) ([]*Worktree, error) {
	return nil, nil
}
func (f *fakeRepo) UpsertCredential(_ context.Context, c *Credential) (*Credential, error) {
	cp := *c
	f.creds[c.WorkspaceID] = &cp
	return &cp, nil
}
func (f *fakeRepo) GetCredentialByWorkspace(_ context.Context, ws uuid.UUID) (*Credential, error) {
	if c, ok := f.creds[ws]; ok {
		cp := *c
		return &cp, nil
	}
	return nil, errs.New(errs.CodeNotFound, "credential not found")
}
func (f *fakeRepo) InTx(_ context.Context, fn func(Tx) error) error { return fn(&fakeTx{f: f}) }

type fakeTx struct{ f *fakeRepo }

func (t *fakeTx) GetWorktreeForUpdate(_ context.Context, ws uuid.UUID, br string) (*Worktree, error) {
	for _, w := range t.f.wts {
		if w.WorkspaceID == ws && w.Branch == br {
			cp := *w
			return &cp, nil
		}
	}
	return nil, &errsNotFound{}
}
func (t *fakeTx) GetWorktreeByID(_ context.Context, id uuid.UUID) (*Worktree, error) {
	if w, ok := t.f.wts[id]; ok {
		cp := *w
		return &cp, nil
	}
	return nil, &errsNotFound{}
}
func (t *fakeTx) GetWorktreeByIDForUpdate(_ context.Context, id uuid.UUID) (*Worktree, error) {
	if w, ok := t.f.wts[id]; ok {
		cp := *w
		return &cp, nil
	}
	return nil, &errsNotFound{}
}
func (t *fakeTx) InsertWorktree(_ context.Context, w *Worktree) (*Worktree, error) {
	cp := *w
	t.f.wts[w.ID] = &cp
	return &cp, nil
}
func (t *fakeTx) SetWorktreeActive(_ context.Context, id uuid.UUID, holder string) (*Worktree, error) {
	w := t.f.wts[id]
	w.Status, w.ActiveHolder = WorktreeStatusActive, holder
	return w, nil
}
func (t *fakeTx) SetWorktreeIdle(_ context.Context, id uuid.UUID) (*Worktree, error) {
	w := t.f.wts[id]
	w.Status, w.ActiveHolder = WorktreeStatusIdle, ""
	return w, nil
}
func (t *fakeTx) SetWorktreePruning(_ context.Context, id uuid.UUID) (*Worktree, error) {
	w := t.f.wts[id]
	w.Status = WorktreeStatusPruning
	return w, nil
}

type errsNotFound struct{}

func (e *errsNotFound) Error() string { return "not found" }

type fakePA struct {
	pid      uuid.UUID
	canRead  bool
	canWrite bool
}

func (f *fakePA) Resolve(_ context.Context, _ uuid.UUID, _ bool, _ string) (uuid.UUID, bool, bool, error) {
	return f.pid, f.canRead, f.canWrite, nil
}
func (f *fakePA) ResolveByID(_ context.Context, _ uuid.UUID, _ bool, _ uuid.UUID) (bool, bool, error) {
	return f.canRead, f.canWrite, nil
}

type fakeSched struct{ called []uuid.UUID }

func (s *fakeSched) Schedule(id uuid.UUID) { s.called = append(s.called, id) }

type fakeGit struct {
	cloneErr  error
	fetchErr  error
	cloneSeen []string
	removed   []string
}

func (f *fakeGit) Clone(_ context.Context, dst, _, _ string, _ *git.Credential) error {
	f.cloneSeen = append(f.cloneSeen, dst)
	return f.cloneErr
}
func (f *fakeGit) Fetch(_ context.Context, _ string, _ *git.Credential) error   { return f.fetchErr }
func (f *fakeGit) Push(_ context.Context, _, _ string, _ *git.Credential) error { return nil }
func (f *fakeGit) Commit(_ context.Context, _, _ string, _ bool) (string, error) {
	return "deadbeef", nil
}
func (f *fakeGit) Status(_ context.Context, _ string) ([]git.FileStatus, error) { return nil, nil }
func (f *fakeGit) MergeFFOnly(_ context.Context, _ string) error                { return nil }
func (f *fakeGit) WorktreeAdd(_ context.Context, _, _, _, _ string) error       { return nil }
func (f *fakeGit) WorktreeRemove(_ context.Context, _, path string) error {
	f.removed = append(f.removed, path)
	return nil
}
func (f *fakeGit) WorktreeList(_ context.Context, _ string) ([]git.Worktree, error) {
	return nil, nil
}
func (f *fakeGit) FetchRemoteBranch(_ context.Context, _, _ string, _ *git.Credential) error {
	return nil
}
func (f *fakeGit) Checkout(_ context.Context, _, _ string) error { return nil }
func (f *fakeGit) ListRemoteBranches(_ context.Context, _ string, _ *git.Credential) ([]string, error) {
	return nil, nil
}
func (f *fakeGit) Log(_ context.Context, _ string, _ int) ([]git.Commit, error) { return nil, nil }
func (f *fakeGit) Diff(_ context.Context, _ string, _ git.DiffOptions) (git.DiffResult, error) {
	return git.DiffResult{}, nil
}
func (f *fakeGit) ListTrackedFiles(_ context.Context, _ string) ([]string, error) { return nil, nil }
func (f *fakeGit) DiffStat(_ context.Context, _, _ string) (string, error)        { return "", nil }

// trackGit 包装 git.Runner，记录 FetchRemoteBranch / Checkout 调用以供断言。
// 嵌入 Runner 接口本身可避免为每个方法重复转发。
type trackGit struct {
	git.Runner
	fetchRemoteBranchCalls []string
	checkoutCalls          []string
}

func (t *trackGit) FetchRemoteBranch(_ context.Context, _, branch string, _ *git.Credential) error {
	t.fetchRemoteBranchCalls = append(t.fetchRemoteBranchCalls, branch)
	return nil
}

func (t *trackGit) Checkout(_ context.Context, _, branch string) error {
	t.checkoutCalls = append(t.checkoutCalls, branch)
	return nil
}

type errFetchGit struct {
	git.Runner
	err error
}

func (e *errFetchGit) FetchRemoteBranch(_ context.Context, _, _ string, _ *git.Credential) error {
	return e.err
}

type errCheckoutGit struct {
	git.Runner
	err error
}

func (e *errCheckoutGit) Checkout(_ context.Context, _, _ string) error { return e.err }

type noopAudit struct{}

func (noopAudit) Record(_ context.Context, _ audit.Entry) error { return nil }

func newSvc(t *testing.T, pa *fakePA, sched CloneScheduler, gitr git.Runner) (*workspaceService, *fakeRepo) {
	t.Helper()
	enc, err := crypto.NewEncryptor(make([]byte, 32))
	require.NoError(t, err)
	repo := newFakeRepo()
	svc := NewWorkspaceService(repo, noopAudit{}, enc, gitr, pa, sched, "/data").(*workspaceService)
	return svc, repo
}

func TestWorkspaceService_Create_HappyPath(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	sched := &fakeSched{}
	svc, repo := newSvc(t, pa, sched, &fakeGit{})
	caller := Caller{UserID: uuid.New()}
	ws, err := svc.Create(context.Background(), caller, "p", CreateWorkspaceInput{
		Slug: "ws1", Name: "W1", GitRemoteURL: "https://x", DefaultBranch: "main",
		Credential: UpsertCredentialInput{Kind: CredKindNone},
	})
	require.NoError(t, err)
	require.Equal(t, SyncStatusCloning, ws.SyncStatus)
	require.Len(t, sched.called, 1)
	require.Equal(t, ws.ID, sched.called[0])
	_, ok := repo.creds[ws.ID]
	require.True(t, ok)
}

func TestWorkspaceService_Create_NilScheduler_NoPanic(t *testing.T) {
	// app 装配有"先建 service（scheduler=nil）→ 再用 service 构 cloneRunner → SetScheduler 回填"的环依赖。
	// Create 必须在 scheduler==nil 时安全跳过 Schedule，否则首个 workspace 会 panic。
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	svc, repo := newSvc(t, pa, nil, &fakeGit{})
	caller := Caller{UserID: uuid.New()}
	ws, err := svc.Create(context.Background(), caller, "p", CreateWorkspaceInput{
		Slug: "ws1", Name: "W1", GitRemoteURL: "https://x", DefaultBranch: "main",
		Credential: UpsertCredentialInput{Kind: CredKindNone},
	})
	require.NoError(t, err)
	require.Equal(t, SyncStatusCloning, ws.SyncStatus)
	_, ok := repo.creds[ws.ID]
	require.True(t, ok)
}

func TestWorkspaceService_SetScheduler_BackfillSchedules(t *testing.T) {
	// SetScheduler 把 nil scheduler 替换为真正实现，下一次 Create 应触发 Schedule。
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	svc, _ := newSvc(t, pa, nil, &fakeGit{})
	sched := &fakeSched{}
	// SchedulerSetter 接口契约：app.go 在构建 CloneRunner 后回填。
	var setter SchedulerSetter = svc
	setter.SetScheduler(sched)
	caller := Caller{UserID: uuid.New()}
	ws, err := svc.Create(context.Background(), caller, "p", CreateWorkspaceInput{
		Slug: "ws2", Name: "W2", GitRemoteURL: "https://x", DefaultBranch: "main",
		Credential: UpsertCredentialInput{Kind: CredKindNone},
	})
	require.NoError(t, err)
	require.Len(t, sched.called, 1)
	require.Equal(t, ws.ID, sched.called[0])
}

func TestWorkspaceService_LoadDecryptedCredential_PassThrough(t *testing.T) {
	// CredentialAccessor 接口暴露内部 loadDecryptedCredential，CloneRunner / GitOpsService 复用。
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	svc, _ := newSvc(t, pa, nil, &fakeGit{})
	var acc CredentialAccessor = svc
	cred, err := acc.LoadDecryptedCredential(context.Background(), uuid.New())
	require.NoError(t, err)
	require.NotNil(t, cred)
	// 没有凭据行 → 返回 git.CredentialNone（loadDecryptedCredential 的兜底）。
	require.Equal(t, git.CredentialNone, cred.Kind)
}

func TestWorkspaceService_Create_BadSlug(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canWrite: true, canRead: true}
	svc, _ := newSvc(t, pa, &fakeSched{}, &fakeGit{})
	_, err := svc.Create(context.Background(), Caller{UserID: uuid.New()}, "p", CreateWorkspaceInput{
		Slug: "BAD-SLUG", Credential: UpsertCredentialInput{Kind: CredKindNone},
	})
	require.Error(t, err)
}

func TestWorkspaceService_Sync_BlockedWhenCloning(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canWrite: true, canRead: true}
	svc, repo := newSvc(t, pa, &fakeSched{}, &fakeGit{})
	id := uuid.New()
	repo.wss[id] = &Workspace{ID: id, ProjectID: pa.pid, MainPath: "/x", SyncStatus: SyncStatusCloning}
	_, err := svc.Sync(context.Background(), Caller{UserID: uuid.New()}, id)
	require.Error(t, err)
}

func TestWorkspaceService_Sync_MissingMainPath_SchedulesReclone(t *testing.T) {
	// main 仓库目录不存在（历史 clone 失败 / 数据卷丢失）时，Sync 不做 fetch，
	// 而是状态切回 cloning 并调度 CloneRunner 重新 clone 自愈。
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canWrite: true, canRead: true}
	sched := &fakeSched{}
	svc, repo := newSvc(t, pa, sched, &fakeGit{})
	id := uuid.New()
	repo.wss[id] = &Workspace{
		ID: id, ProjectID: pa.pid, SyncStatus: SyncStatusError,
		MainPath: filepath.Join(t.TempDir(), "gone", "main"),
	}
	ws, err := svc.Sync(context.Background(), Caller{UserID: uuid.New()}, id)
	require.NoError(t, err)
	require.Equal(t, SyncStatusCloning, ws.SyncStatus)
	require.Equal(t, []uuid.UUID{id}, sched.called)
}

func TestCloneRunner_HappyPath(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canWrite: true, canRead: true}
	gitr := &fakeGit{}
	svc, repo := newSvc(t, pa, &fakeSched{}, gitr)
	id := uuid.New()
	repo.wss[id] = &Workspace{ID: id, MainPath: "/data/main", DefaultBranch: "main", SyncStatus: SyncStatusCloning}
	runner := NewCloneRunner(repo, noopAudit{}, gitr, slog.Default(),
		func(ctx context.Context, wsID uuid.UUID) (*git.Credential, error) {
			return svc.loadDecryptedCredential(ctx, wsID)
		}, 0)
	runner.Schedule(id)
	runner.Wait()
	got, _ := repo.GetWorkspaceByID(context.Background(), id)
	require.Equal(t, SyncStatusIdle, got.SyncStatus)
}

func TestCloneRunner_Failure(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canWrite: true, canRead: true}
	gitr := &fakeGit{cloneErr: errors.New("boom")}
	svc, repo := newSvc(t, pa, &fakeSched{}, gitr)
	id := uuid.New()
	repo.wss[id] = &Workspace{ID: id, MainPath: "/x", DefaultBranch: "main", SyncStatus: SyncStatusCloning}
	runner := NewCloneRunner(repo, noopAudit{}, gitr, slog.Default(),
		func(ctx context.Context, wsID uuid.UUID) (*git.Credential, error) {
			return svc.loadDecryptedCredential(ctx, wsID)
		}, 0)
	runner.Schedule(id)
	runner.Wait()
	got, _ := repo.GetWorkspaceByID(context.Background(), id)
	require.Equal(t, SyncStatusError, got.SyncStatus)
	require.Contains(t, got.LastSyncError, "boom")
}

func TestWorkspaceService_Update_DefaultBranch_Switch(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	fg := &trackGit{Runner: &fakeGit{}}
	svc, repo := newSvc(t, pa, nil, fg)
	caller := Caller{UserID: uuid.New()}

	// Seed a workspace with main_path set.
	wsID := uuid.New()
	existing := &Workspace{
		ID: wsID, ProjectID: pa.pid, Slug: "ws1", Name: "W1",
		DefaultBranch: "main", MainPath: "/data/ws1/main",
	}
	repo.wss[wsID] = existing

	newBr := "develop"
	_, err := svc.Update(context.Background(), caller, wsID, UpdateWorkspaceInput{
		DefaultBranch: &newBr,
	})
	require.NoError(t, err)
	require.Equal(t, "develop", repo.wss[wsID].DefaultBranch)
	require.Equal(t, []string{"develop"}, fg.fetchRemoteBranchCalls)
	require.Equal(t, []string{"develop"}, fg.checkoutCalls)
}

func TestWorkspaceService_Update_DefaultBranch_FetchFails(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	fg := &errFetchGit{Runner: &fakeGit{}, err: errors.New("remote branch not found")}
	svc, repo := newSvc(t, pa, nil, fg)
	caller := Caller{UserID: uuid.New()}

	wsID := uuid.New()
	repo.wss[wsID] = &Workspace{
		ID: wsID, ProjectID: pa.pid, Slug: "ws1", Name: "W1",
		DefaultBranch: "main", MainPath: "/data/ws1/main",
	}

	newBr := "nonexistent"
	_, err := svc.Update(context.Background(), caller, wsID, UpdateWorkspaceInput{
		DefaultBranch: &newBr,
	})
	require.Error(t, err)
	// DB should NOT be updated
	require.Equal(t, "main", repo.wss[wsID].DefaultBranch)
}

func TestWorkspaceService_Update_DefaultBranch_CheckoutFails(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	fg := &errCheckoutGit{Runner: &fakeGit{}, err: errors.New("dirty working tree")}
	svc, repo := newSvc(t, pa, nil, fg)
	caller := Caller{UserID: uuid.New()}

	wsID := uuid.New()
	repo.wss[wsID] = &Workspace{
		ID: wsID, ProjectID: pa.pid, Slug: "ws1", Name: "W1",
		DefaultBranch: "main", MainPath: "/data/ws1/main",
	}

	newBr := "develop"
	_, err := svc.Update(context.Background(), caller, wsID, UpdateWorkspaceInput{
		DefaultBranch: &newBr,
	})
	require.Error(t, err)
	// DB should NOT be updated
	require.Equal(t, "main", repo.wss[wsID].DefaultBranch)
}

func TestWorkspaceService_Update_DefaultBranch_SameValue_NoGitOps(t *testing.T) {
	// 把 default_branch 更新成相同值时，不应触发任何 git 操作（避免无谓的 fetch/checkout）。
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	fg := &trackGit{Runner: &fakeGit{}}
	svc, repo := newSvc(t, pa, nil, fg)
	caller := Caller{UserID: uuid.New()}

	wsID := uuid.New()
	repo.wss[wsID] = &Workspace{
		ID: wsID, ProjectID: pa.pid, Slug: "ws1", Name: "W1",
		DefaultBranch: "main", MainPath: "/data/ws1/main",
	}

	sameBr := "main"
	_, err := svc.Update(context.Background(), caller, wsID, UpdateWorkspaceInput{
		DefaultBranch: &sameBr,
	})
	require.NoError(t, err)
	require.Empty(t, fg.fetchRemoteBranchCalls)
	require.Empty(t, fg.checkoutCalls)
}

func TestWorkspaceService_ListBranches(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true}
	svc, repo := newSvc(t, pa, nil, &fakeGit{})
	caller := Caller{UserID: uuid.New()}

	wsID := uuid.New()
	repo.wss[wsID] = &Workspace{
		ID: wsID, ProjectID: pa.pid, Slug: "ws1",
		DefaultBranch: "main", MainPath: t.TempDir(),
	}

	branches, err := svc.ListBranches(context.Background(), caller, wsID)
	require.NoError(t, err)
	// fakeGit returns nil, nil → empty slice
	require.Nil(t, branches)
}

func TestWorkspaceService_ListBranches_NoReadAccess(t *testing.T) {
	t.Parallel()
	pa := &fakePA{pid: uuid.New(), canRead: false, canWrite: false}
	svc, repo := newSvc(t, pa, nil, &fakeGit{})
	caller := Caller{UserID: uuid.New()}

	wsID := uuid.New()
	repo.wss[wsID] = &Workspace{
		ID: wsID, ProjectID: pa.pid, Slug: "ws1",
	}

	_, err := svc.ListBranches(context.Background(), caller, wsID)
	require.Error(t, err)
}
