/**
 * Worktree fix contract tests — vitest unit tests for worktree module fix contracts.
 *
 * Purpose: prevent regression on the worktree+git cluster fixes.
 * Consumer: CI gate via `npm run test:unit -- 'worktree-fixes'`.
 */

import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	symlinkSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, test, vi } from "vitest";

// ─── Top-level mock state for native-git-bridge.js ─────────────────────────

const gitMock = vi.hoisted(() => ({
	nativeDetectMainBranchReturn: "main",
	nativeIsAncestorReturn: false,
	nativeHasChangesReturn: false,
	nativeWorkingTreeStatusReturn: "",
	nativeUnpushedCountReturn: 0,
	nativeLastCommitEpochReturn: 0,
}));

vi.mock("../native-git-bridge.js", () => ({
	nativeDetectMainBranch: vi.fn(() => gitMock.nativeDetectMainBranchReturn),
	nativeIsAncestor: vi.fn(() => gitMock.nativeIsAncestorReturn),
	nativeHasChanges: vi.fn(() => gitMock.nativeHasChangesReturn),
	nativeWorkingTreeStatus: vi.fn(() => gitMock.nativeWorkingTreeStatusReturn),
	nativeUnpushedCount: vi.fn(() => gitMock.nativeUnpushedCountReturn),
	nativeLastCommitEpoch: vi.fn(() => gitMock.nativeLastCommitEpochReturn),
}));

// ─── Helpers ───────────────────────────────────────────────────────────────

function makeTempDir(prefix) {
	return mkdtempSync(join(tmpdir(), prefix));
}

function cleanup(dir) {
	try {
		rmSync(dir, { recursive: true, force: true });
	} catch {}
}

function resetGitMocks() {
	gitMock.nativeDetectMainBranchReturn = "main";
	gitMock.nativeIsAncestorReturn = false;
	gitMock.nativeHasChangesReturn = false;
	gitMock.nativeWorkingTreeStatusReturn = "";
	gitMock.nativeUnpushedCountReturn = 0;
	gitMock.nativeLastCommitEpochReturn = 0;
}

// ─── resolveGitDir (worktree-manager.js) ───────────────────────────────────

describe("resolveGitDir", () => {
	test("normal repo: .git is directory -> returns join(base, '.git')", async () => {
		const base = makeTempDir("sf-wt-test-");
		mkdirSync(join(base, ".git"));
		const { resolveGitDir } = await import("../worktree-manager.js");
		const result = resolveGitDir(base);
		expect(result).toBe(join(base, ".git"));
		cleanup(base);
	});

	test("worktree: .git is pointer file -> resolves gitdir path", async () => {
		const base = makeTempDir("sf-wt-test-");
		writeFileSync(join(base, ".git"), "gitdir: /repo/.git/worktrees/project\n");
		const { resolveGitDir } = await import("../worktree-manager.js");
		const result = resolveGitDir(base);
		expect(result).toBe("/repo/.git/worktrees/project");
		cleanup(base);
	});

	test("missing .git: returns join(base, '.git')", async () => {
		const base = makeTempDir("sf-wt-test-");
		const { resolveGitDir } = await import("../worktree-manager.js");
		const result = resolveGitDir(base);
		expect(result).toBe(join(base, ".git"));
		cleanup(base);
	});
});

// ─── getWorktreeHealth (worktree-health.js) ────────────────────────────────

describe("getWorktreeHealth", () => {
	test("broken symlink target: lstatSync succeeds, existsSync fails -> reports pathAccessible=false", async () => {
		resetGitMocks();
		const base = makeTempDir("sf-wt-test-");
		const wtPath = join(base, "wt1");
		// Create a symlink pointing to a non-existent target
		symlinkSync("/nonexistent/path", wtPath);

		const { getWorktreeHealth } = await import("../worktree-health.js");
		const wt = {
			name: "wt1",
			path: wtPath,
			branch: "worktree/wt1",
			exists: true,
		};
		const result = getWorktreeHealth(base, wt);
		expect(result.pathAccessible).toBe(false);
		expect(result.dirty).toBe(false);
		cleanup(base);
	});

	test("valid worktree: reports mergedIntoMain, dirty, stale correctly", async () => {
		resetGitMocks();
		gitMock.nativeIsAncestorReturn = true;
		gitMock.nativeHasChangesReturn = true;
		gitMock.nativeWorkingTreeStatusReturn = "M file1.ts\nM file2.ts";
		gitMock.nativeUnpushedCountReturn = 2;
		gitMock.nativeLastCommitEpochReturn =
			Math.floor(Date.now() / 1000) - 86400 * 20;

		const base = makeTempDir("sf-wt-test-");
		const wtPath = join(base, "wt1");
		mkdirSync(wtPath);

		const { getWorktreeHealth } = await import("../worktree-health.js");
		const wt = {
			name: "wt1",
			path: wtPath,
			branch: "worktree/wt1",
			exists: true,
		};
		const result = getWorktreeHealth(base, wt);
		expect(result.mergedIntoMain).toBe(true);
		expect(result.dirty).toBe(true);
		expect(result.dirtyFileCount).toBe(2);
		expect(result.unpushedCommits).toBe(2);
		expect(result.stale).toBe(false);
		expect(result.safeToRemove).toBe(false);
		cleanup(base);
	});

	test("inaccessible path (ENOENT): returns safeToRemove=false", async () => {
		resetGitMocks();
		const base = makeTempDir("sf-wt-test-");
		const wtPath = join(base, "wt1");
		// Path does not exist

		const { getWorktreeHealth } = await import("../worktree-health.js");
		const wt = {
			name: "wt1",
			path: wtPath,
			branch: "worktree/wt1",
			exists: true,
		};
		const result = getWorktreeHealth(base, wt);
		expect(result.pathAccessible).toBe(false);
		expect(result.safeToRemove).toBe(false);
		cleanup(base);
	});
});

// ─── getActiveWorktreeName path extraction ─────────────────────────────────

describe("getActiveWorktreeName path extraction", () => {
	test("path with forward slashes: extracts name correctly", () => {
		const rel = "/my-wt/src".replace(/^[\\/]+/, "");
		const name = rel.split(/[\\/]/)[0] ?? rel;
		expect(name).toBe("my-wt");
	});

	test("path with backslashes (Windows): extracts name correctly", () => {
		const rel = "\\my-wt\\src".replace(/^[\\/]+/, "");
		const name = rel.split(/[\\/]/)[0] ?? rel;
		expect(name).toBe("my-wt");
	});

	test("path with trailing backslash: does not return empty string", () => {
		const rel = "\\my-wt\\".replace(/^[\\/]+/, "");
		const parts = rel.split(/[\\/]/);
		const name = parts[0] ?? rel;
		expect(name).toBe("my-wt");
		expect(name).not.toBe("");
	});
});

// ─── projectRoot capture (worktree-resolver.js) ────────────────────────────

describe("WorktreeResolver.enterMilestone", () => {
	test("projectRoot captured BEFORE basePath mutated", async () => {
		const captured = [];
		vi.doMock("../journal.js", () => ({
			emitJournalEvent: vi.fn((projectRoot, event) => {
				captured.push({ projectRoot, event });
			}),
		}));
		vi.doMock("../worktree-telemetry.js", () => ({
			emitCanonicalRootRedirect: vi.fn(),
			emitWorktreeDivergenceWarning: vi.fn(),
			emitWorktreeCreated: vi.fn(),
			emitWorktreeMerged: vi.fn(),
		}));
		vi.doMock("../debug-logger.js", () => ({
			debugLog: vi.fn(),
		}));
		vi.doMock("../preferences.js", () => ({
			loadEffectiveSFPreferences: vi.fn(() => ({})),
		}));
		vi.doMock("../slice-cadence.js", () => ({
			getCollapseCadence: vi.fn(() => "slice"),
			getMilestoneResquash: vi.fn(() => false),
			resquashMilestoneOnMain: vi.fn(() => ({ resquashed: false })),
		}));
		vi.doMock("../git-service.js", () => ({
			MergeConflictError: class MergeConflictError extends Error {},
			inferCommitType: vi.fn(() => "feat"),
		}));

		vi.resetModules();
		const { WorktreeResolver } = await import("../worktree-resolver.js");
		const mockSession = {
			basePath: "/project",
			originalBasePath: null,
			isolationDegraded: false,
			milestoneStartShas: new Map(),
			gitService: null,
		};
		const mockDeps = {
			shouldUseWorktreeIsolation: () => true,
			getAutoWorktreePath: () => "/project/.sf/worktrees/M001",
			enterAutoWorktree: () => "/project/.sf/worktrees/M001",
			createAutoWorktree: () => "/project/.sf/worktrees/M001",
			GitService: class MockGitService {},
			loadEffectiveSFPreferences: () => ({}),
			invalidateAllCaches: () => {},
		};
		const resolver = new WorktreeResolver(mockSession, mockDeps);
		resolver.enterMilestone("M001", { notify: () => {} });
		expect(captured.length).toBeGreaterThan(0);
		expect(captured[0].projectRoot).toBe("/project");
		expect(mockSession.basePath).toBe("/project/.sf/worktrees/M001");
		vi.doUnmock("../journal.js");
		vi.doUnmock("../worktree-telemetry.js");
		vi.doUnmock("../debug-logger.js");
		vi.doUnmock("../preferences.js");
		vi.doUnmock("../slice-cadence.js");
		vi.doUnmock("../git-service.js");
	});
});

// ─── originalCwd clear-on-success (worktree-command.js) ────────────────────

describe("originalCwd lifecycle", () => {
	test("merge succeeds: originalCwd cleared via clearWorktreeOriginalCwd()", () => {
		const __dirname = dirname(fileURLToPath(import.meta.url));
		const sourcePath = join(__dirname, "..", "worktree-command.js");
		const source = readFileSync(sourcePath, "utf-8");
		const mergeSuccessPattern =
			/mergeWorktreeToMain\([^)]+\);\s*\n\s*\/\/ Merge succeeded[^\n]*\n\s*clearWorktreeOriginalCwd\(\);/;
		expect(source).toMatch(mergeSuccessPattern);
	});

	test("merge fails before chdir: originalCwd remains set", () => {
		const __dirname = dirname(fileURLToPath(import.meta.url));
		const sourcePath = join(__dirname, "..", "worktree-command.js");
		const source = readFileSync(sourcePath, "utf-8");
		const catchBlockPattern = /catch \(mergeErr\)[\s\S]{0,800}/;
		const catchMatch = source.match(catchBlockPattern);
		expect(catchMatch).toBeTruthy();
		const catchBlock = catchMatch[0];
		expect(catchBlock).not.toMatch(/originalCwd\s*=\s*null/);
	});
});
