/**
 * Tests for replaceSymbol and insertAroundSymbol.
 *
 * These tests require the native addon to be compiled.  They are designed but
 * will fail at runtime until:
 *   1. The integration owner adds `mod symbol;` to `lib.rs`.
 *   2. The native addon is rebuilt (`npm run build:native -w @singularity-forge/native`).
 *
 * Run with:
 *   npx vitest run packages/native/src/__tests__/symbol.test.mjs --config vitest.config.ts
 */

import assert from "node:assert/strict";
import * as fs from "node:fs";
import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);

const addonDir = path.resolve(
	__dirname,
	"..",
	"..",
	"..",
	"..",
	"rust-engine",
	"addon",
);
const platformTag = `${process.platform}-${process.arch}`;
const candidates = [
	path.join(addonDir, "forge_engine.dev.node"),
	path.join(addonDir, `forge_engine.${platformTag}.node`),
];

let native;
for (const candidate of candidates) {
	try {
		native = require(candidate);
		break;
	} catch {
		// try next
	}
}

if (!native) {
	console.error(
		"Native addon not found. Run `npm run build:native -w @singularity-forge/native` first.",
	);
	process.exit(1);
}

// ── helpers ───────────────────────────────────────────────────────────────────

function tempTsFile(contents) {
	const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sf-symbol-test-"));
	const file = path.join(dir, "sample.ts");
	fs.writeFileSync(file, contents, "utf8");
	return { dir, file };
}

// ── replaceSymbol ─────────────────────────────────────────────────────────────

describe("native symbol: replaceSymbol()", () => {
	test("replaces a top-level TypeScript function declaration", ({
		onTestFinished,
	}) => {
		const original = `function foo() { return 1; }\n`;
		const { dir, file } = tempTsFile(original);
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		const result = native.replaceSymbol(
			file,
			"foo",
			"function foo() { return 42; }",
			{ fsync: false },
		);

		assert.equal(result.matched, true);
		assert.ok(typeof result.byteStart === "number", "byteStart should be set");
		assert.ok(typeof result.startLine === "number", "startLine should be set");
		assert.equal(result.startLine, 1, "startLine should be 1-based");

		const written = fs.readFileSync(file, "utf8");
		assert.ok(
			written.includes("return 42"),
			`Expected new body in file, got: ${written}`,
		);
		assert.ok(
			!written.includes("return 1;"),
			`Old body should be gone, got: ${written}`,
		);
	});

	test("returns matched:false when the symbol does not exist", ({
		onTestFinished,
	}) => {
		const { dir, file } = tempTsFile("function bar() { return 0; }\n");
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		const result = native.replaceSymbol(
			file,
			"nonExistentFunction",
			"function nonExistentFunction() {}",
			{ fsync: false },
		);

		assert.equal(result.matched, false);
		assert.equal(result.byteStart, undefined);
		// File must be unchanged.
		assert.ok(fs.readFileSync(file, "utf8").includes("function bar()"));
	});

	test("throws on ambiguous symbol (multiple matches)", ({
		onTestFinished,
	}) => {
		// Two functions with different names but both match a loose pattern is not
		// the ambiguity scenario; the ambiguity arises from an overloaded symbol.
		// Simulate it by writing a file with the same function name appearing twice
		// (which can happen if tree-sitter picks up both the function and an arrow
		// alias with the same logical name via multiple patterns — or in practice
		// when the user uses duplicate declarations).
		const source =
			[
				"function executeCommand() { return 1; }",
				"const executeCommand = () => { return 2; }",
			].join("\n") + "\n";
		const { dir, file } = tempTsFile(source);
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		assert.throws(
			() =>
				native.replaceSymbol(
					file,
					"executeCommand",
					"function executeCommand() { return 99; }",
					{ fsync: false },
				),
			/[Aa]mbiguous/,
		);
		// File must be unchanged.
		assert.ok(fs.readFileSync(file, "utf8").includes("return 1;"));
	});

	test("replaces an arrow function declaration", ({ onTestFinished }) => {
		const { dir, file } = tempTsFile(
			"const greet = (name: string) => { return `Hello " + "$" + "{name}`; }\n",
		);
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		const result = native.replaceSymbol(
			file,
			"greet",
			"const greet = (name: string) => { return `Hi " + "$" + "{name}!`; }",
			{ fsync: false },
		);

		assert.equal(result.matched, true);
		const written = fs.readFileSync(file, "utf8");
		assert.ok(written.includes("Hi"), `Expected new body, got: ${written}`);
	});
});

// ── insertAroundSymbol ────────────────────────────────────────────────────────

describe("native symbol: insertAroundSymbol()", () => {
	test("BeforeDecl inserts code on the line immediately before the declaration", ({
		onTestFinished,
	}) => {
		const original = "function hello() { return 'world'; }\n";
		const { dir, file } = tempTsFile(original);
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		const result = native.insertAroundSymbol(
			file,
			"hello",
			"BeforeDecl",
			"// AUTO-GENERATED\n",
			{ fsync: false },
		);

		assert.equal(result.inserted, true);
		assert.ok(
			typeof result.byteOffset === "number",
			"byteOffset should be set",
		);
		assert.equal(
			result.byteOffset,
			0,
			"should insert at start of file for top-level decl",
		);

		const written = fs.readFileSync(file, "utf8");
		assert.ok(
			written.startsWith("// AUTO-GENERATED\n"),
			`Expected comment at top, got: ${written}`,
		);
		assert.ok(
			written.includes("function hello()"),
			"Original declaration must still exist",
		);
	});

	test("AfterDecl inserts code immediately after the declaration", ({
		onTestFinished,
	}) => {
		const original = "function bye() { return 'cya'; }\n";
		const { dir, file } = tempTsFile(original);
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		const result = native.insertAroundSymbol(
			file,
			"bye",
			"AfterDecl",
			"\n// end of bye\n",
			{ fsync: false },
		);

		assert.equal(result.inserted, true);
		const written = fs.readFileSync(file, "utf8");
		assert.ok(
			written.includes("// end of bye"),
			`Expected trailing comment, got: ${written}`,
		);
	});

	test("returns inserted:false when symbol does not exist", ({
		onTestFinished,
	}) => {
		const { dir, file } = tempTsFile("function noop() {}\n");
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		const result = native.insertAroundSymbol(
			file,
			"phantom",
			"BeforeDecl",
			"// phantom\n",
			{ fsync: false },
		);

		assert.equal(result.inserted, false);
		assert.equal(result.byteOffset, undefined);
		assert.equal(fs.readFileSync(file, "utf8"), "function noop() {}\n");
	});

	test("AtBodyStart and AtBodyEnd throw 'not yet implemented'", ({
		onTestFinished,
	}) => {
		const { dir, file } = tempTsFile("function x() { return 1; }\n");
		onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));

		for (const pos of ["AtBodyStart", "AtBodyEnd"]) {
			assert.throws(
				() =>
					native.insertAroundSymbol(file, "x", pos, "/* code */", {
						fsync: false,
					}),
				/not yet implemented/i,
				`Expected 'not yet implemented' error for position ${pos}`,
			);
		}
	});
});
