/**
 * SPEC-170 — Pre-built Worktree Lifecycle Managed by ReviewFlow
 *
 * Spec: docs/specs/170-prebuilt-worktree-lifecycle.md
 * Plan: docs/plans/170-prebuilt-worktree-lifecycle.plan.md
 *
 * Outer-loop acceptance test (SDD): mirrors the 11 scenarios defined in
 * the spec's `## Scenarios` block. All scenarios assert at the use-case
 * boundary (ensureWorktree / removeWorktree / sweepStaleWorktrees) using
 * `StubGitCommandExecutor`, matching the shape of scenario 9 already
 * shipped in PR #175.
 */
import { vi } from 'vitest';
vi.mock('@/frameworks/config/configLoader.js', () => ({
    loadConfig: vi.fn(() => ({
        queue: { maxConcurrent: 4, deduplicationWindowMs: 60000 },
    })),
}));
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { removeWorktree } from '../../modules/worktree-management/usecases/removeWorktree.usecase.js';
import { StubGitCommandExecutor } from '../../tests/stubs/gitCommandExecutor.stub.js';
import { deriveWorktreePath } from '../../modules/worktree-management/entities/worktree/worktree.js';
import { enqueueReview, initQueue } from '../../frameworks/queue/pQueueAdapter.js';
import { createStubLogger } from '../../tests/stubs/logger.stub.js';
import { buildMcpSystemPrompt } from '../../frameworks/claude/claudeInvoker.js';
import { startWorktreeSweepScheduler } from '../../frameworks/scheduler/worktreeSweepScheduler.js';
import { ensureWorktree } from '../../modules/worktree-management/usecases/ensureWorktree.usecase.js';
const baseIdentity = {
    platform: 'gitlab',
    projectPath: 'test-org/test-project',
    mrNumber: 42,
};
const sourceCheckoutPath = '/home/user/projects/test-project';
describe('Acceptance — SPEC-170: Pre-built Worktree Lifecycle', () => {
    describe('Feature: Worktree ensure-or-reuse on review dispatch', () => {
        it('Scenario 1 — first review on new MR: ensureWorktree prunes + fetches branch + worktree-add + writes settings; returns created', async () => {
            const executor = new StubGitCommandExecutor();
            const writeSettingsCalls = [];
            const result = await ensureWorktree({
                identity: baseIdentity,
                sourceBranch: 'feat/x',
                source: { kind: 'origin' },
                sourceCheckoutPath,
            }, {
                executor,
                worktreeExists: async () => false,
                writeWorktreeSettings: async (path) => {
                    writeSettingsCalls.push(path);
                    return { status: 'ok' };
                },
            });
            const expectedPath = deriveWorktreePath(baseIdentity);
            expect(result).toEqual({
                status: 'created',
                path: expectedPath,
                settingsWarning: null,
            });
            const pruneCalls = executor.callsOfKind('worktree-prune');
            expect(pruneCalls).toHaveLength(1);
            expect(pruneCalls[0]?.cwd).toBe(sourceCheckoutPath);
            const fetchCalls = executor.callsOfKind('fetch');
            expect(fetchCalls).toHaveLength(1);
            expect(fetchCalls[0]?.args).toEqual(['fetch', 'origin', 'feat/x']);
            expect(fetchCalls[0]?.cwd).toBe(sourceCheckoutPath);
            const addCalls = executor.callsOfKind('worktree-add');
            expect(addCalls).toHaveLength(1);
            expect(addCalls[0]?.args).toEqual(['worktree', 'add', expectedPath, 'origin/feat/x']);
            expect(addCalls[0]?.cwd).toBe(sourceCheckoutPath);
            expect(executor.callsOfKind('reset-hard')).toHaveLength(0);
            expect(writeSettingsCalls).toEqual([expectedPath]);
        });
        it('Scenario 2 — followup on existing MR: ensureWorktree prunes + fetches inside worktree + reset --hard (no worktree-add, no settings rewrite); returns reused', async () => {
            const executor = new StubGitCommandExecutor();
            const writeSettingsCalls = [];
            const result = await ensureWorktree({
                identity: baseIdentity,
                sourceBranch: 'feat/x',
                source: { kind: 'origin' },
                sourceCheckoutPath,
            }, {
                executor,
                worktreeExists: async () => true,
                writeWorktreeSettings: async (path) => {
                    writeSettingsCalls.push(path);
                    return { status: 'ok' };
                },
            });
            const expectedPath = deriveWorktreePath(baseIdentity);
            expect(result).toEqual({ status: 'reused', path: expectedPath });
            const pruneCalls = executor.callsOfKind('worktree-prune');
            expect(pruneCalls).toHaveLength(1);
            expect(pruneCalls[0]?.cwd).toBe(sourceCheckoutPath);
            const fetchCalls = executor.callsOfKind('fetch');
            expect(fetchCalls).toHaveLength(1);
            expect(fetchCalls[0]?.args).toEqual(['fetch', 'origin', 'feat/x']);
            expect(fetchCalls[0]?.cwd).toBe(expectedPath);
            const resetCalls = executor.callsOfKind('reset-hard');
            expect(resetCalls).toHaveLength(1);
            expect(resetCalls[0]?.args).toEqual(['reset', '--hard', 'origin/feat/x']);
            expect(resetCalls[0]?.cwd).toBe(expectedPath);
            expect(executor.callsOfKind('worktree-add')).toHaveLength(0);
            expect(writeSettingsCalls).toEqual([]);
        });
    });
    describe('Feature: Worktree cleanup on MR close', () => {
        it('Scenario 3 — merge cleanup: removeWorktree on merged identity returns removed', async () => {
            const executor = new StubGitCommandExecutor();
            const worktreeExistsByPath = new Map();
            const result = await removeWorktree({ identity: { ...baseIdentity }, sourceCheckoutPath }, {
                executor,
                worktreeExists: async (path) => {
                    if (!worktreeExistsByPath.has(path)) {
                        worktreeExistsByPath.set(path, true);
                    }
                    return worktreeExistsByPath.get(path) ?? false;
                },
            });
            expect(result.status).toBe('removed');
            expect(executor.callsOfKind('worktree-prune').length).toBeGreaterThan(0);
            expect(executor.callsOfKind('worktree-remove').length).toBe(1);
        });
        it('Scenario 4 — close cleanup: removeWorktree on closed identity returns removed', async () => {
            const executor = new StubGitCommandExecutor();
            const result = await removeWorktree({
                identity: { platform: 'github', projectPath: 'owner/repo', mrNumber: 7 },
                sourceCheckoutPath,
            }, {
                executor,
                worktreeExists: async () => true,
            });
            expect(result.status).toBe('removed');
        });
        it('Scenario 5 — merge with worktree already gone: returns absent, no remove call, no throw', async () => {
            const executor = new StubGitCommandExecutor();
            const result = await removeWorktree({ identity: { ...baseIdentity }, sourceCheckoutPath }, {
                executor,
                worktreeExists: async () => false,
            });
            expect(result.status).toBe('absent');
            expect(executor.callsOfKind('worktree-remove').length).toBe(0);
        });
    });
    describe('Feature: Daily safety-net sweep', () => {
        const sweepRepository = { localPath: '/repos/test-project', enabled: true };
        const sweepNow = new Date('2026-05-23T12:00:00Z');
        function buildTrackedMrFixture(overrides) {
            return {
                id: overrides.id ?? 'gitlab-test-org/test-project-1',
                mrNumber: overrides.mrNumber ?? 1,
                title: 'title',
                url: 'http://example.com',
                project: overrides.project ?? 'test-org/test-project',
                platform: overrides.platform ?? 'gitlab',
                sourceBranch: 'feat/x',
                targetBranch: 'master',
                assignment: { username: 'user', assignedAt: '2026-05-01T00:00:00Z' },
                state: overrides.state ?? 'pending-review',
                openThreads: 0,
                totalThreads: 0,
                createdAt: '2026-05-01T00:00:00Z',
                lastReviewAt: null,
                lastPushAt: null,
                approvedAt: null,
                mergedAt: overrides.mergedAt ?? null,
                reviews: [],
                totalReviews: 0,
                totalFollowups: 0,
                totalBlocking: 0,
                totalWarnings: 0,
                totalSuggestions: 0,
                totalDurationMs: 0,
                latestScore: null,
                autoFollowup: true,
                bypass: null,
            };
        }
        function buildSweepStubGateway(entries) {
            let liveEntries = [...entries];
            const removed = [];
            const gateway = {
                list: async () => liveEntries,
                remove: async (request) => {
                    removed.push(request.identity);
                    liveEntries = liveEntries.filter(entry => deriveWorktreePath(entry.identity) !== deriveWorktreePath(request.identity));
                    return { status: 'removed' };
                },
                ensure: async () => ({ status: 'failed', reason: 'not-used-in-sweep' }),
                exists: async () => false,
            };
            return {
                gateway,
                get removed() {
                    return removed;
                },
            };
        }
        beforeEach(() => {
            vi.useFakeTimers();
            vi.setSystemTime(sweepNow);
        });
        afterEach(() => {
            vi.useRealTimers();
        });
        it('Scenario 6 — closed MR over 24h: worktree present + tracker merged 48h ago → remove worktree', async () => {
            const identity = { platform: 'gitlab', projectPath: 'test-org/test-project', mrNumber: 1 };
            const entry = {
                identity,
                path: deriveWorktreePath(identity),
                mtime: new Date('2026-05-21T00:00:00Z'),
            };
            const stub = buildSweepStubGateway([entry]);
            const mrId = `${identity.platform}-${identity.projectPath}-${identity.mrNumber}`;
            const tracked = buildTrackedMrFixture({
                id: mrId,
                state: 'merged',
                mergedAt: '2026-05-21T00:00:00Z',
            });
            const handle = startWorktreeSweepScheduler({
                worktreeGateway: stub.gateway,
                trackingGateway: {
                    getById: (projectPath, requestedId) => projectPath === sweepRepository.localPath && requestedId === mrId ? tracked : null,
                },
                getRepositories: () => [sweepRepository],
                logger: createStubLogger(),
                now: () => sweepNow,
            });
            await vi.advanceTimersByTimeAsync(10);
            handle.stop();
            expect(stub.removed).toEqual([identity]);
        });
        it('Scenario 7 — orphan: worktree present + no tracked MR → remove worktree', async () => {
            const identity = { platform: 'gitlab', projectPath: 'test-org/test-project', mrNumber: 2 };
            const entry = {
                identity,
                path: deriveWorktreePath(identity),
                mtime: new Date('2026-05-23T11:00:00Z'),
            };
            const stub = buildSweepStubGateway([entry]);
            const handle = startWorktreeSweepScheduler({
                worktreeGateway: stub.gateway,
                trackingGateway: { getById: () => null },
                getRepositories: () => [sweepRepository],
                logger: createStubLogger(),
                now: () => sweepNow,
            });
            await vi.advanceTimersByTimeAsync(10);
            handle.stop();
            expect(stub.removed).toEqual([identity]);
        });
        it('Scenario 8 — stale active MR: worktree mtime 8 days old + tracker pending-review → remove worktree', async () => {
            const identity = { platform: 'gitlab', projectPath: 'test-org/test-project', mrNumber: 3 };
            const eightDaysAgo = new Date(sweepNow.getTime() - 8 * 24 * 60 * 60 * 1000);
            const entry = {
                identity,
                path: deriveWorktreePath(identity),
                mtime: eightDaysAgo,
            };
            const stub = buildSweepStubGateway([entry]);
            const mrId = `${identity.platform}-${identity.projectPath}-${identity.mrNumber}`;
            const tracked = buildTrackedMrFixture({
                id: mrId,
                mrNumber: identity.mrNumber,
                state: 'pending-review',
            });
            const handle = startWorktreeSweepScheduler({
                worktreeGateway: stub.gateway,
                trackingGateway: {
                    getById: (projectPath, requestedId) => projectPath === sweepRepository.localPath && requestedId === mrId ? tracked : null,
                },
                getRepositories: () => [sweepRepository],
                logger: createStubLogger(),
                now: () => sweepNow,
            });
            await vi.advanceTimersByTimeAsync(10);
            handle.stop();
            expect(stub.removed).toEqual([identity]);
        });
    });
    describe('Feature: GitHub cross-fork PR handling', () => {
        it('Scenario 9 — cross-fork PR: ensureWorktree fetches from fork URL with refspec patch-1:refs/remotes/pr-N/head and worktree-add from refs/remotes/pr-N/head', async () => {
            const forkCloneUrl = 'https://github.com/contributor/test-repo.git';
            const identity = {
                platform: 'github',
                projectPath: 'test-owner/test-repo',
                mrNumber: 77,
            };
            const sourceBranch = 'patch-1';
            const executor = new StubGitCommandExecutor();
            const result = await ensureWorktree({
                identity,
                sourceBranch,
                source: { kind: 'fork', cloneUrl: forkCloneUrl },
                sourceCheckoutPath: '/home/user/projects/test-repo',
            }, {
                executor,
                worktreeExists: async () => false,
                writeWorktreeSettings: async () => ({ status: 'ok' }),
            });
            expect(result.status).toBe('created');
            const fetchCalls = executor.callsOfKind('fetch');
            expect(fetchCalls).toHaveLength(1);
            expect(fetchCalls[0]?.args).toEqual([
                'fetch',
                forkCloneUrl,
                `${sourceBranch}:refs/remotes/pr-${identity.mrNumber}/head`,
            ]);
            const addCalls = executor.callsOfKind('worktree-add');
            expect(addCalls).toHaveLength(1);
            expect(addCalls[0]?.args).toEqual([
                'worktree',
                'add',
                deriveWorktreePath(identity),
                `refs/remotes/pr-${identity.mrNumber}/head`,
            ]);
        });
    });
    describe('Feature: Per-MR serialization of concurrent operations', () => {
        it('Scenario 10 — concurrent followups on same MR: second waits for first; both complete in order', async () => {
            initQueue(createStubLogger());
            const events = [];
            let releaseFirst = null;
            const baseJob = {
                id: 'gitlab:test-org/test-project:99',
                platform: 'gitlab',
                projectPath: 'test-org/test-project',
                localPath: '/home/user/projects/test-project',
                mrNumber: 99,
                skill: 'review-front',
                mrUrl: 'https://gitlab.com/test-org/test-project/-/merge_requests/99',
                sourceBranch: 'feature/test',
                targetBranch: 'main',
                jobType: 'review',
            };
            const freshEnqueued = await enqueueReview(baseJob, async () => {
                events.push('fresh:start');
                await new Promise(resolve => {
                    releaseFirst = resolve;
                });
                events.push('fresh:end');
            });
            expect(freshEnqueued).toBe(true);
            const followupJob = {
                ...baseJob,
                id: 'gitlab-followup:test-org/test-project:99',
                jobType: 'followup',
            };
            const followupEnqueued = await enqueueReview(followupJob, async () => {
                events.push('followup:start');
                events.push('followup:end');
            });
            expect(followupEnqueued).toBe(true);
            await new Promise(resolve => setTimeout(resolve, 20));
            expect(events).toEqual(['fresh:start']);
            releaseFirst();
            await new Promise(resolve => setTimeout(resolve, 50));
            expect(events).toEqual(['fresh:start', 'fresh:end', 'followup:start', 'followup:end']);
        });
    });
    describe('Feature: System prompt no longer disclaims local state', () => {
        it('Scenario 11 — system prompt contains no UNRELIABLE / FORBIDDEN / glab mr diff / gh pr diff substrings', () => {
            const job = {
                id: 'gitlab:test-org/test-project:42',
                platform: 'gitlab',
                projectPath: 'test-org/test-project',
                localPath: '/home/user/projects/test-project',
                mrNumber: 42,
                skill: 'review-front',
                mrUrl: 'https://gitlab.com/test-org/test-project/-/merge_requests/42',
                sourceBranch: 'feat/x',
                targetBranch: 'main',
                jobType: 'review',
            };
            const prompt = buildMcpSystemPrompt(job);
            expect(prompt).not.toContain('UNRELIABLE');
            expect(prompt).not.toContain('FORBIDDEN');
            expect(prompt).not.toContain('glab mr diff');
            expect(prompt).not.toContain('gh pr diff');
        });
    });
});
//# sourceMappingURL=170-prebuilt-worktree-lifecycle.acceptance.test.js.map