/**
 * worktree-divergence.test.ts — #6 divergence cap enforcement
 *
 * Tests for:
 * - getCommitsBehindMain returns correct counts
 * - mergeWorktreeToMain emits divergence-warning when commits-behind > threshold
 * - mergeWorktreeToMain proceeds with merge even when over threshold (warn-and-proceed)
 * - mergeWorktreeToMain({ autoRebase: true }) rebases and merges when no conflicts
 * - mergeWorktreeToMain({ autoRebase: true }) throws merge conflict and leaves worktree
 *   in conflict state when rebase conflicts
 * - Below-threshold case: no warning emitted
 */

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, test } from "vitest";

import { queryJournal } from "../journal.js";
import { getCommitsBehindMain } from "../native-git-bridge.js";
import {
	mergeWorktreeToMain,
	WORKTREE_DIVERGENCE_CAP,
	worktreePath,
} from "../worktree-manager.js";

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

function git(args: string[], cwd: string): string {
	return execFileSync("git", args, {
		cwd,
		stdio: ["ignore", "pipe", "pipe"],
		encoding: "utf-8",
		env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
	}).trim();
}

/** Create a minimal git repo on `main` with one initial commit. */
function makeBaseRepo(): string {
	const base = mkdtempSync(join(tmpdir(), "sf-div-test-"));
	git(["init", "-b", "main"], base);
	git(["config", "user.name", "Test User"], base);
	git(["config", "user.email", "test@test.com"], base);
	// Create .sf dir so journal can write
	mkdirSync(join(base, ".sf"), { recursive: true });
	writeFileSync(join(base, "README.md"), "initial\n");
	git(["add", "."], base);
	git(["commit", "-m", "chore: init"], base);
	return base;
}

/**
 * Add a git worktree at `.sf/worktrees/<name>` on branch `worktree/<name>`.
 * Returns the worktree path.
 */
function addWorktree(base: string, name: string): string {
	const wtDir = join(base, ".sf", "worktrees", name);
	mkdirSync(join(base, ".sf", "worktrees"), { recursive: true });
	git(["worktree", "add", "-b", `worktree/${name}`, wtDir], base);
	return wtDir;
}

/** Commit a file change in a directory. */
function commitFile(
	dir: string,
	filename: string,
	content: string,
	message: string,
): void {
	writeFileSync(join(dir, filename), content);
	git(["add", filename], dir);
	git(["commit", "-m", message], dir);
}

// ─── getCommitsBehindMain ─────────────────────────────────────────────────────

describe("getCommitsBehindMain", () => {
	let base: string;
	let wtPath: string;

	beforeEach(() => {
		base = makeBaseRepo();
		wtPath = addWorktree(base, "M001");
	});

	afterEach(() => {
		rmSync(base, { recursive: true, force: true });
	});

	test("returns zero behind when worktree is up-to-date with main", () => {
		const { commitsAhead, commitsBehind } = getCommitsBehindMain(wtPath, "main");
		assert.equal(commitsBehind, 0);
		assert.equal(commitsAhead, 0);
	});

	test("returns correct commitsBehind when main has advanced", () => {
		// Advance main by 3 commits (worktree does NOT pick these up)
		commitFile(base, "a.txt", "a\n", "chore: A");
		commitFile(base, "b.txt", "b\n", "chore: B");
		commitFile(base, "c.txt", "c\n", "chore: C");

		const { commitsBehind, commitsAhead } = getCommitsBehindMain(wtPath, "main");
		assert.equal(commitsBehind, 3);
		assert.equal(commitsAhead, 0);
	});

	test("returns correct commitsAhead when worktree has advanced", () => {
		// Advance worktree by 2 commits
		commitFile(wtPath, "x.txt", "x\n", "feat: X");
		commitFile(wtPath, "y.txt", "y\n", "feat: Y");

		const { commitsAhead, commitsBehind } = getCommitsBehindMain(wtPath, "main");
		assert.equal(commitsAhead, 2);
		assert.equal(commitsBehind, 0);
	});

	test("returns both counts when both have diverged", () => {
		// Advance main by 2
		commitFile(base, "m1.txt", "m1\n", "chore: main1");
		commitFile(base, "m2.txt", "m2\n", "chore: main2");
		// Advance worktree by 1
		commitFile(wtPath, "w1.txt", "w1\n", "feat: worktree1");

		const { commitsAhead, commitsBehind } = getCommitsBehindMain(wtPath, "main");
		assert.equal(commitsBehind, 2);
		assert.equal(commitsAhead, 1);
	});
});

// ─── mergeWorktreeToMain — divergence warning ─────────────────────────────────

describe("mergeWorktreeToMain divergence warning", () => {
	let base: string;

	beforeEach(() => {
		base = makeBaseRepo();
	});

	afterEach(() => {
		rmSync(base, { recursive: true, force: true });
	});

	test("emits worktree-divergence-warning when commitsBehind > threshold", () => {
		const name = "M002";
		const wtDir = addWorktree(base, name);

		// Advance main by WORKTREE_DIVERGENCE_CAP + 1 commits (worktree stays behind)
		for (let i = 0; i <= WORKTREE_DIVERGENCE_CAP; i++) {
			commitFile(base, `main-${i}.txt`, `${i}\n`, `chore: main advance ${i}`);
		}

		// Add one commit to worktree so there is something to merge
		commitFile(wtDir, "feature.txt", "feature\n", "feat: add feature");

		// Perform merge (warn-and-proceed — may fail on conflict since main diverged)
		try {
			mergeWorktreeToMain(base, name, "feat: merge M002");
		} catch {
			// A merge conflict is fine for this test; we only care about the warning event
		}

		const entries = queryJournal(base);
		const warning = entries.find(
			(e) => e.eventType === "worktree-divergence-warning",
		);
		assert.ok(warning, "worktree-divergence-warning event must be emitted");
		assert.equal(warning.data?.worktreeId, name);
		assert.ok(
			typeof warning.data?.commitsBehind === "number" &&
				(warning.data.commitsBehind as number) > WORKTREE_DIVERGENCE_CAP,
			"commitsBehind should exceed the threshold",
		);
		assert.equal(warning.data?.threshold, WORKTREE_DIVERGENCE_CAP);
	});

	test("does NOT emit worktree-divergence-warning when below threshold", () => {
		const name = "M003";
		const wtDir = addWorktree(base, name);

		// Advance main by only 2 commits (well below cap)
		commitFile(base, "m1.txt", "m1\n", "chore: m1");
		commitFile(base, "m2.txt", "m2\n", "chore: m2");

		// Add a commit to the worktree that doesn't conflict
		commitFile(wtDir, "feature2.txt", "feature2\n", "feat: feature2");

		try {
			mergeWorktreeToMain(base, name, "feat: merge M003");
		} catch {
			// ignore merge issues
		}

		const entries = queryJournal(base);
		const warning = entries.find(
			(e) => e.eventType === "worktree-divergence-warning",
		);
		assert.equal(
			warning,
			undefined,
			"no warning event should be emitted below threshold",
		);
	});
});

// ─── mergeWorktreeToMain — warn-and-proceed ───────────────────────────────────

describe("mergeWorktreeToMain warn-and-proceed", () => {
	let base: string;

	beforeEach(() => {
		base = makeBaseRepo();
	});

	afterEach(() => {
		rmSync(base, { recursive: true, force: true });
	});

	test("proceeds with merge (no conflict) even when over threshold", () => {
		const name = "M004";
		const wtDir = addWorktree(base, name);

		// Advance main on a different file (no conflict)
		for (let i = 0; i <= WORKTREE_DIVERGENCE_CAP; i++) {
			commitFile(base, `main-nc-${i}.txt`, `${i}\n`, `chore: no-conflict ${i}`);
		}

		// Worktree adds a new file only it touches
		commitFile(wtDir, "worktree-only.txt", "wt\n", "feat: worktree-only");

		// Should succeed despite being over threshold
		const result = mergeWorktreeToMain(base, name, "feat: merge M004");
		assert.equal(result, "feat: merge M004");

		// Verify the warning was emitted
		const entries = queryJournal(base);
		const warning = entries.find(
			(e) => e.eventType === "worktree-divergence-warning",
		);
		assert.ok(warning, "warning must be emitted");
	});
});

// ─── mergeWorktreeToMain — autoRebase ─────────────────────────────────────────

describe("mergeWorktreeToMain autoRebase", () => {
	let base: string;

	beforeEach(() => {
		base = makeBaseRepo();
	});

	afterEach(() => {
		rmSync(base, { recursive: true, force: true });
	});

	test("autoRebase:true rebases and merges successfully when no conflicts", () => {
		const name = "M005";
		const wtDir = addWorktree(base, name);

		// Advance main by 3 commits on unique files
		commitFile(base, "main-r1.txt", "r1\n", "chore: rebase-r1");
		commitFile(base, "main-r2.txt", "r2\n", "chore: rebase-r2");
		commitFile(base, "main-r3.txt", "r3\n", "chore: rebase-r3");

		// Worktree adds a unique file
		commitFile(wtDir, "wt-feature.txt", "feature\n", "feat: wt-feature");

		const result = mergeWorktreeToMain(base, name, "feat: merge M005 rebased", {
			autoRebase: true,
		});
		assert.equal(result, "feat: merge M005 rebased");

		// Confirm the merge landed on main
		const log = git(["log", "--oneline", "-1"], base);
		assert.ok(log.includes("feat: merge M005 rebased"), "commit should be on main");

		// The rebase file should exist in main after the squash-merge
		assert.ok(
			existsSync(join(base, "wt-feature.txt")),
			"worktree file should exist in main after merge",
		);
	});

	test("autoRebase:true throws SF_MERGE_CONFLICT when rebase has conflicts", () => {
		const name = "M006";
		const wtDir = addWorktree(base, name);

		// Both main and worktree modify the same file at the same line
		commitFile(base, "conflict.txt", "main version\n", "chore: main conflict");
		commitFile(wtDir, "conflict.txt", "worktree version\n", "feat: wt conflict");

		let threw = false;
		let errorMessage = "";
		try {
			mergeWorktreeToMain(base, name, "feat: merge M006 rebase-conflict", {
				autoRebase: true,
			});
		} catch (err: unknown) {
			threw = true;
			errorMessage = err instanceof Error ? err.message : String(err);
		}

		assert.ok(threw, "should throw when rebase has conflicts");
		assert.ok(
			errorMessage.includes("conflict") || errorMessage.includes("Conflict"),
			`error message should mention conflict, got: ${errorMessage}`,
		);
	});
});
