import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { afterAll, describe, test } from "vitest";
import { detectMonorepo } from "../../web/bridge-service.ts";
import { discoverProjects } from "../../web/project-discovery-service.ts";

// ---------------------------------------------------------------------------
// Fixture setup — standard multi-project root
// ---------------------------------------------------------------------------

const tempRoot = mkdtempSync(join(tmpdir(), "sf-project-discovery-"));

// project-a: brownfield (package.json + .git)
const projectA = join(tempRoot, "project-a");
mkdirSync(projectA);
mkdirSync(join(projectA, ".git"));
writeFileSync(join(projectA, "package.json"), "{}");

// project-b: empty-sf (.sf folder, no milestones)
const projectB = join(tempRoot, "project-b");
mkdirSync(projectB);
mkdirSync(join(projectB, ".sf"));

// project-c: brownfield (Cargo.toml)
const projectC = join(tempRoot, "project-c");
mkdirSync(projectC);
writeFileSync(join(projectC, "Cargo.toml"), "");

// project-d: blank (empty)
const projectD = join(tempRoot, "project-d");
mkdirSync(projectD);

// .hidden: should be excluded
mkdirSync(join(tempRoot, ".hidden"));

// node_modules: should be excluded
mkdirSync(join(tempRoot, "node_modules"));

// ---------------------------------------------------------------------------
// Fixture setup — monorepo roots
// ---------------------------------------------------------------------------

// monorepo-pnpm: detected via pnpm-workspace.yaml
const monorepoPnpm = mkdtempSync(join(tmpdir(), "sf-mono-pnpm-"));
mkdirSync(join(monorepoPnpm, ".git"));
writeFileSync(join(monorepoPnpm, "package.json"), '{"name":"my-monorepo"}');
writeFileSync(
	join(monorepoPnpm, "pnpm-workspace.yaml"),
	'packages:\n  - "packages/*"',
);
mkdirSync(join(monorepoPnpm, "packages"));
mkdirSync(join(monorepoPnpm, "packages", "pkg-a"));
mkdirSync(join(monorepoPnpm, "packages", "pkg-b"));

// monorepo-lerna: detected via lerna.json
const monorepoLerna = mkdtempSync(join(tmpdir(), "sf-mono-lerna-"));
mkdirSync(join(monorepoLerna, ".git"));
writeFileSync(join(monorepoLerna, "package.json"), '{"name":"lerna-mono"}');
writeFileSync(join(monorepoLerna, "lerna.json"), '{"version":"1.0.0"}');
mkdirSync(join(monorepoLerna, "backend"));
mkdirSync(join(monorepoLerna, "frontend"));

// monorepo-workspaces: detected via package.json workspaces field
const monorepoWorkspaces = mkdtempSync(join(tmpdir(), "sf-mono-ws-"));
mkdirSync(join(monorepoWorkspaces, ".git"));
writeFileSync(
	join(monorepoWorkspaces, "package.json"),
	'{"name":"ws-mono","workspaces":["packages/*"]}',
);
mkdirSync(join(monorepoWorkspaces, "packages"));
mkdirSync(join(monorepoWorkspaces, "packages", "core"));
mkdirSync(join(monorepoWorkspaces, "packages", "ui"));

// monorepo-turbo: detected via turbo.json
const monorepoTurbo = mkdtempSync(join(tmpdir(), "sf-mono-turbo-"));
mkdirSync(join(monorepoTurbo, ".git"));
writeFileSync(join(monorepoTurbo, "package.json"), '{"name":"turbo-mono"}');
writeFileSync(join(monorepoTurbo, "turbo.json"), '{"pipeline":{}}');
mkdirSync(join(monorepoTurbo, "apps"));
mkdirSync(join(monorepoTurbo, "packages"));

// monorepo-nx: detected via nx.json
const monorepoNx = mkdtempSync(join(tmpdir(), "sf-mono-nx-"));
mkdirSync(join(monorepoNx, ".git"));
writeFileSync(join(monorepoNx, "package.json"), '{"name":"nx-mono"}');
writeFileSync(join(monorepoNx, "nx.json"), "{}");
mkdirSync(join(monorepoNx, "libs"));
mkdirSync(join(monorepoNx, "apps"));

// non-monorepo: plain project with package.json (no workspaces, no marker files)
const plainProject = mkdtempSync(join(tmpdir(), "sf-plain-project-"));
mkdirSync(join(plainProject, ".git"));
writeFileSync(
	join(plainProject, "package.json"),
	'{"name":"plain","dependencies":{}}',
);
mkdirSync(join(plainProject, "src"));

// ---------------------------------------------------------------------------
// Teardown
// ---------------------------------------------------------------------------

afterAll(() => {
	rmSync(tempRoot, { recursive: true, force: true });
	rmSync(monorepoPnpm, { recursive: true, force: true });
	rmSync(monorepoLerna, { recursive: true, force: true });
	rmSync(monorepoWorkspaces, { recursive: true, force: true });
	rmSync(monorepoTurbo, { recursive: true, force: true });
	rmSync(monorepoNx, { recursive: true, force: true });
	rmSync(plainProject, { recursive: true, force: true });
});

// ---------------------------------------------------------------------------
// Tests — standard multi-project root
// ---------------------------------------------------------------------------

describe("project-discovery", () => {
	test("discovers exactly 4 project directories (excludes hidden + node_modules)", () => {
		const results = discoverProjects(tempRoot);
		assert.equal(
			results.length,
			4,
			`Expected 4 projects, got ${results.length}: ${results.map((r) => r.name).join(", ")}`,
		);
	});

	test("results are sorted alphabetically by name", () => {
		const results = discoverProjects(tempRoot);
		const names = results.map((r) => r.name);
		assert.deepStrictEqual(names, [
			"project-a",
			"project-b",
			"project-c",
			"project-d",
		]);
	});

	test("project-a is detected as brownfield with correct signals", () => {
		const results = discoverProjects(tempRoot);
		const a = results.find((r) => r.name === "project-a");
		assert.ok(a, "project-a not found");
		assert.equal(a.kind, "brownfield");
		assert.equal(a.signals.hasPackageJson, true);
		assert.equal(a.signals.hasGitRepo, true);
	});

	test("project-b is detected as empty-sf", () => {
		const results = discoverProjects(tempRoot);
		const b = results.find((r) => r.name === "project-b");
		assert.ok(b, "project-b not found");
		assert.equal(b.kind, "empty-sf");
		assert.equal(b.signals.hasSfFolder, true);
	});

	test("project-c is detected as brownfield with hasCargo signal", () => {
		const results = discoverProjects(tempRoot);
		const c = results.find((r) => r.name === "project-c");
		assert.ok(c, "project-c not found");
		assert.equal(c.kind, "brownfield");
		assert.equal(c.signals.hasCargo, true);
	});

	test("project-d is detected as blank", () => {
		const results = discoverProjects(tempRoot);
		const d = results.find((r) => r.name === "project-d");
		assert.ok(d, "project-d not found");
		assert.equal(d.kind, "blank");
	});

	test("excludes .hidden and node_modules directories", () => {
		const results = discoverProjects(tempRoot);
		const names = results.map((r) => r.name);
		assert.ok(!names.includes(".hidden"), ".hidden should be excluded");
		assert.ok(
			!names.includes("node_modules"),
			"node_modules should be excluded",
		);
	});

	test("all entries have lastModified as a number > 0", () => {
		const results = discoverProjects(tempRoot);
		for (const entry of results) {
			assert.equal(typeof entry.lastModified, "number");
			assert.ok(
				entry.lastModified > 0,
				`${entry.name} lastModified should be > 0`,
			);
		}
	});

	test("all entries have valid path and name", () => {
		const results = discoverProjects(tempRoot);
		for (const entry of results) {
			assert.ok(
				entry.path.startsWith(tempRoot),
				`${entry.name} path should start with tempRoot`,
			);
			assert.ok(entry.name.length > 0, "name should not be empty");
		}
	});

	test("nonexistent path returns empty array", () => {
		const results = discoverProjects("/nonexistent/path/that/does/not/exist");
		assert.deepStrictEqual(results, []);
	});
});

// ---------------------------------------------------------------------------
// Tests — monorepo detection
// ---------------------------------------------------------------------------

describe("detectMonorepo", () => {
	test("detects pnpm-workspace.yaml", () => {
		assert.ok(detectMonorepo(monorepoPnpm));
	});

	test("detects lerna.json", () => {
		assert.ok(detectMonorepo(monorepoLerna));
	});

	test("detects package.json with workspaces field", () => {
		assert.ok(detectMonorepo(monorepoWorkspaces));
	});

	test("detects turbo.json", () => {
		assert.ok(detectMonorepo(monorepoTurbo));
	});

	test("detects nx.json", () => {
		assert.ok(detectMonorepo(monorepoNx));
	});

	test("does not detect plain project as monorepo", () => {
		assert.ok(!detectMonorepo(plainProject));
	});

	test("does not detect empty directory as monorepo", () => {
		assert.ok(!detectMonorepo(tempRoot));
	});
});

// ---------------------------------------------------------------------------
// Tests — monorepo root as devRoot returns single entry
// ---------------------------------------------------------------------------

describe("project-discovery with monorepo root as devRoot", () => {
	test("pnpm monorepo root returns single project entry", () => {
		const results = discoverProjects(monorepoPnpm);
		assert.equal(
			results.length,
			1,
			`Expected 1 project, got ${results.length}: ${results.map((r) => r.name).join(", ")}`,
		);
		assert.equal(results[0].path, monorepoPnpm);
		assert.equal(results[0].name, basename(monorepoPnpm));
		assert.equal(results[0].signals.isMonorepo, true);
	});

	test("lerna monorepo root returns single project entry", () => {
		const results = discoverProjects(monorepoLerna);
		assert.equal(results.length, 1);
		assert.equal(results[0].path, monorepoLerna);
		assert.equal(results[0].signals.isMonorepo, true);
	});

	test("npm/yarn workspaces monorepo root returns single project entry", () => {
		const results = discoverProjects(monorepoWorkspaces);
		assert.equal(results.length, 1);
		assert.equal(results[0].path, monorepoWorkspaces);
		assert.equal(results[0].signals.isMonorepo, true);
	});

	test("turbo monorepo root returns single project entry", () => {
		const results = discoverProjects(monorepoTurbo);
		assert.equal(results.length, 1);
		assert.equal(results[0].path, monorepoTurbo);
	});

	test("nx monorepo root returns single project entry", () => {
		const results = discoverProjects(monorepoNx);
		assert.equal(results.length, 1);
		assert.equal(results[0].path, monorepoNx);
	});

	test("plain project (not monorepo) scans children normally", () => {
		// plainProject has .git, package.json, src/ — not a monorepo
		// Should scan children: just "src"
		const results = discoverProjects(plainProject);
		assert.ok(results.length >= 1, "should scan children for non-monorepo");
		assert.ok(
			results.some((r) => r.name === "src"),
			"should find src directory",
		);
	});

	test("monorepo entry has correct kind (brownfield when no .sf)", () => {
		const results = discoverProjects(monorepoPnpm);
		assert.equal(results[0].kind, "brownfield");
	});
});
