import assert from "node:assert/strict";
import { mkdirSync } from "node:fs";
import { test } from "vitest";
import {
	buildSliceFileName,
	resolveMilestonePath,
	resolveSliceFile,
	resolveSlicePath,
} from "../paths.js";
import {
	computeOverallScore,
	deriveCounts,
	EVAL_REVIEW_SCHEMA_VERSION,
	EvalReviewFrontmatter,
	extractFrontmatterRaw,
	parseEvalReviewFrontmatter,
	verdictForScore,
} from "../eval-review-schema.js";
import {
	buildEvalReviewContext,
	detectEvalReviewState,
	EvalReviewArgError,
	evalReviewWritePath,
	findEvalReviewFile,
	parseEvalReviewArgs,
	planEvalReviewAction,
	SLICE_ID_PATTERN,
} from "../commands-eval-review.js";
import { TOP_LEVEL_SUBCOMMANDS } from "../commands/catalog.js";
import { makeTempDir, cleanup, createFile } from "./test-utils.js";

// ─── Argument parsing ───────────────────────────────────────────────────────

test("parseEvalReviewArgs: valid slice ID only", () => {
	const args = parseEvalReviewArgs("S07");
	assert.equal(args.sliceId, "S07");
	assert.equal(args.force, false);
	assert.equal(args.show, false);
});

test("parseEvalReviewArgs: with --force", () => {
	const args = parseEvalReviewArgs("S07 --force");
	assert.equal(args.sliceId, "S07");
	assert.equal(args.force, true);
	assert.equal(args.show, false);
});

test("parseEvalReviewArgs: with --show", () => {
	const args = parseEvalReviewArgs("S07 --show");
	assert.equal(args.sliceId, "S07");
	assert.equal(args.force, false);
	assert.equal(args.show, true);
});

test("parseEvalReviewArgs: rejects missing slice ID", () => {
	assert.throws(() => parseEvalReviewArgs(""), EvalReviewArgError);
	assert.throws(() => parseEvalReviewArgs("--force"), EvalReviewArgError);
});

test("parseEvalReviewArgs: rejects invalid slice ID", () => {
	assert.throws(() => parseEvalReviewArgs("../etc/passwd"), EvalReviewArgError);
	assert.throws(() => parseEvalReviewArgs("foo"), EvalReviewArgError);
	assert.throws(() => parseEvalReviewArgs("S"), EvalReviewArgError);
});

test("parseEvalReviewArgs: rejects unknown flags", () => {
	assert.throws(() => parseEvalReviewArgs("S07 --wipe"), EvalReviewArgError);
});

test("parseEvalReviewArgs: rejects multiple slice IDs", () => {
	assert.throws(() => parseEvalReviewArgs("S07 S08"), EvalReviewArgError);
});

// ─── Slice ID pattern ───────────────────────────────────────────────────────

test("SLICE_ID_PATTERN: accepts canonical IDs", () => {
	assert.ok(SLICE_ID_PATTERN.test("S01"));
	assert.ok(SLICE_ID_PATTERN.test("S99"));
	assert.ok(SLICE_ID_PATTERN.test("S123"));
});

test("SLICE_ID_PATTERN: rejects path traversal", () => {
	assert.ok(!SLICE_ID_PATTERN.test("../foo"));
	assert.ok(!SLICE_ID_PATTERN.test("S01/../../etc"));
	assert.ok(!SLICE_ID_PATTERN.test("foo/bar"));
});

test("SLICE_ID_PATTERN: rejects malformed IDs", () => {
	assert.ok(!SLICE_ID_PATTERN.test("S"));
	assert.ok(!SLICE_ID_PATTERN.test("s01"));
	assert.ok(!SLICE_ID_PATTERN.test("01"));
	assert.ok(!SLICE_ID_PATTERN.test("S01a"));
	assert.ok(!SLICE_ID_PATTERN.test(""));
});

// ─── State detection ────────────────────────────────────────────────────────

test("detectEvalReviewState: no-slice-dir when slice missing", () => {
	const dir = makeTempDir();
	try {
		mkdirSync(`${dir}/.sf/milestones/M001/slices`, { recursive: true });
		const args = parseEvalReviewArgs("S99");
		const state = detectEvalReviewState(args, dir, "M001");
		assert.equal(state.kind, "no-slice-dir");
		if (state.kind === "no-slice-dir") {
			assert.ok(state.expectedDir.includes("S99"));
		}
	} finally {
		cleanup(dir);
	}
});

test("detectEvalReviewState: no-summary when SUMMARY.md missing", () => {
	const dir = makeTempDir();
	try {
		mkdirSync(`${dir}/.sf/milestones/M001/slices/S01`, { recursive: true });
		const args = parseEvalReviewArgs("S01");
		const state = detectEvalReviewState(args, dir, "M001");
		assert.equal(state.kind, "no-summary");
	} finally {
		cleanup(dir);
	}
});

test("detectEvalReviewState: ready when SUMMARY.md present", () => {
	const dir = makeTempDir();
	try {
		createFile(dir, ".sf/milestones/M001/slices/S01/S01-SUMMARY.md", "# Summary\n");
		const args = parseEvalReviewArgs("S01");
		const state = detectEvalReviewState(args, dir, "M001");
		assert.equal(state.kind, "ready");
		if (state.kind === "ready") {
			assert.ok(state.summaryPath.includes("S01-SUMMARY.md"));
		}
	} finally {
		cleanup(dir);
	}
});

// ─── Path helpers ───────────────────────────────────────────────────────────

test("evalReviewWritePath: canonical naming", () => {
	const path = evalReviewWritePath("/tmp/milestone/slices/S01", "S01");
	assert.ok(path.endsWith("S01-EVAL-REVIEW.md"));
	assert.ok(path.includes("/tmp/milestone/slices/S01"));
});

test("findEvalReviewFile: returns null when absent", () => {
	const dir = makeTempDir();
	try {
		mkdirSync(`${dir}/.sf/milestones/M001/slices/S01`, { recursive: true });
		const result = findEvalReviewFile(dir, "M001", "S01");
		assert.equal(result, null);
	} finally {
		cleanup(dir);
	}
});

test("findEvalReviewFile: returns path when present", () => {
	const dir = makeTempDir();
	try {
		createFile(dir, ".sf/milestones/M001/slices/S01/S01-EVAL-REVIEW.md", "---\n---\n");
		const result = findEvalReviewFile(dir, "M001", "S01");
		assert.ok(result !== null);
		assert.ok(result?.includes("S01-EVAL-REVIEW.md"));
	} finally {
		cleanup(dir);
	}
});

// ─── Action planner ─────────────────────────────────────────────────────────

test("planEvalReviewAction: no-slice-dir → no-slice-dir", () => {
	const args = parseEvalReviewArgs("S01");
	const state = { kind: "no-slice-dir", sliceId: "S01", expectedDir: "/tmp" } as const;
	const action = planEvalReviewAction(args, state, null);
	assert.equal(action.kind, "no-slice-dir");
});

test("planEvalReviewAction: --show tolerates missing summary", () => {
	const args = parseEvalReviewArgs("S01 --show");
	const state = { kind: "no-summary", sliceId: "S01", sliceDir: "/tmp", specPath: null } as const;
	const action = planEvalReviewAction(args, state, null);
	assert.equal(action.kind, "show");
	assert.equal(action.path, null);
});

test("planEvalReviewAction: no-summary without --show → no-summary", () => {
	const args = parseEvalReviewArgs("S01");
	const state = { kind: "no-summary", sliceId: "S01", sliceDir: "/tmp", specPath: null } as const;
	const action = planEvalReviewAction(args, state, null);
	assert.equal(action.kind, "no-summary");
});

test("planEvalReviewAction: existing file without --force → exists-no-force", () => {
	const args = parseEvalReviewArgs("S01");
	const state = { kind: "ready", sliceId: "S01", sliceDir: "/tmp", summaryPath: "/tmp/SUMMARY.md", specPath: null } as const;
	const action = planEvalReviewAction(args, state, "/tmp/S01-EVAL-REVIEW.md");
	assert.equal(action.kind, "exists-no-force");
});

test("planEvalReviewAction: existing file with --force → dispatch", () => {
	const args = parseEvalReviewArgs("S01 --force");
	const state = { kind: "ready", sliceId: "S01", sliceDir: "/tmp", summaryPath: "/tmp/SUMMARY.md", specPath: null } as const;
	const action = planEvalReviewAction(args, state, "/tmp/S01-EVAL-REVIEW.md");
	assert.equal(action.kind, "dispatch");
});

test("planEvalReviewAction: no existing file → dispatch", () => {
	const args = parseEvalReviewArgs("S01");
	const state = { kind: "ready", sliceId: "S01", sliceDir: "/tmp", summaryPath: "/tmp/SUMMARY.md", specPath: null } as const;
	const action = planEvalReviewAction(args, state, null);
	assert.equal(action.kind, "dispatch");
});

// ─── Context builder ────────────────────────────────────────────────────────

test("buildEvalReviewContext: reads SUMMARY.md and builds context", async () => {
	const dir = makeTempDir();
	try {
		createFile(dir, ".sf/milestones/M001/slices/S01/S01-SUMMARY.md", "# Summary\nDone.\n");
		const state = {
			kind: "ready" as const,
			sliceId: "S01",
			sliceDir: `${dir}/.sf/milestones/M001/slices/S01`,
			summaryPath: `${dir}/.sf/milestones/M001/slices/S01/S01-SUMMARY.md`,
			specPath: null,
		};
		const ctx = await buildEvalReviewContext(state, "M001", () => new Date("2024-01-01T00:00:00Z"));
		assert.equal(ctx.milestoneId, "M001");
		assert.equal(ctx.sliceId, "S01");
		assert.ok(ctx.summary.includes("Done."));
		assert.equal(ctx.spec, null);
		assert.equal(ctx.truncated, false);
		assert.ok(ctx.outputPath.includes("S01-EVAL-REVIEW.md"));
	} finally {
		cleanup(dir);
	}
});

test("buildEvalReviewContext: reads optional AI-SPEC.md", async () => {
	const dir = makeTempDir();
	try {
		createFile(dir, ".sf/milestones/M001/slices/S01/S01-SUMMARY.md", "# Summary\n");
		createFile(dir, ".sf/milestones/M001/slices/S01/S01-AI-SPEC.md", "# Spec\n");
		const state = {
			kind: "ready" as const,
			sliceId: "S01",
			sliceDir: `${dir}/.sf/milestones/M001/slices/S01`,
			summaryPath: `${dir}/.sf/milestones/M001/slices/S01/S01-SUMMARY.md`,
			specPath: `${dir}/.sf/milestones/M001/slices/S01/S01-AI-SPEC.md`,
		};
		const ctx = await buildEvalReviewContext(state, "M001");
		assert.ok(ctx.spec?.includes("# Spec"));
	} finally {
		cleanup(dir);
	}
});

// ─── Schema / frontmatter ───────────────────────────────────────────────────

test("extractFrontmatterRaw: finds YAML block", () => {
	const raw = "---\nschema: eval-review/v1\nverdict: PRODUCTION_READY\n---\n# Body\n";
	const result = extractFrontmatterRaw(raw);
	assert.ok("yaml" in result);
	if ("yaml" in result) {
		assert.ok(result.yaml.includes("schema: eval-review/v1"));
	}
});

test("extractFrontmatterRaw: rejects missing opening delimiter", () => {
	const result = extractFrontmatterRaw("no frontmatter");
	assert.ok("error" in result);
});

test("extractFrontmatterRaw: rejects missing closing delimiter", () => {
	const result = extractFrontmatterRaw("---\nschema: eval-review/v1\n");
	assert.ok("error" in result);
});

test("parseEvalReviewFrontmatter: valid frontmatter parses", () => {
	const raw = `---
schema: eval-review/v1
verdict: PRODUCTION_READY
coverage_score: 80
infrastructure_score: 75
overall_score: 78
generated: 2024-01-01T00:00:00Z
slice: S01
milestone: M001
gaps: []
counts:
  blocker: 0
  major: 0
  minor: 0
---
# Body
`;
	const result = parseEvalReviewFrontmatter(raw);
	assert.ok(result.ok);
	if (result.ok) {
		assert.equal(result.data.schema, EVAL_REVIEW_SCHEMA_VERSION);
		assert.equal(result.data.verdict, "PRODUCTION_READY");
		assert.equal(result.data.coverage_score, 80);
		assert.equal(result.data.gaps.length, 0);
	}
});

test("parseEvalReviewFrontmatter: invalid schema version fails", () => {
	const raw = `---
schema: eval-review/v2
verdict: PRODUCTION_READY
coverage_score: 80
infrastructure_score: 75
overall_score: 78
generated: 2024-01-01T00:00:00Z
slice: S01
milestone: M001
gaps: []
counts:
  blocker: 0
  major: 0
  minor: 0
---
`;
	const result = parseEvalReviewFrontmatter(raw);
	assert.ok(!result.ok);
});

test("parseEvalReviewFrontmatter: missing frontmatter fails", () => {
	const result = parseEvalReviewFrontmatter("# Just a markdown file\n");
	assert.ok(!result.ok);
});

// ─── Derived fields ─────────────────────────────────────────────────────────

test("computeOverallScore: 60/40 weighting", () => {
	assert.equal(computeOverallScore(80, 75), Math.round(80 * 0.6 + 75 * 0.4));
	assert.equal(computeOverallScore(100, 0), 60);
	assert.equal(computeOverallScore(0, 100), 40);
	assert.equal(computeOverallScore(50, 50), 50);
});

test("computeOverallScore: clamps to bounds", () => {
	assert.equal(computeOverallScore(-10, -10), 0);
	assert.equal(computeOverallScore(200, 200), 100);
});

test("deriveCounts: tallies severities correctly", () => {
	const gaps = [
		{ id: "G01", dimension: "tests", severity: "blocker", description: "d", evidence: "e", suggested_fix: "f" },
		{ id: "G02", dimension: "tests", severity: "major", description: "d", evidence: "e", suggested_fix: "f" },
		{ id: "G03", dimension: "tests", severity: "major", description: "d", evidence: "e", suggested_fix: "f" },
		{ id: "G04", dimension: "tests", severity: "minor", description: "d", evidence: "e", suggested_fix: "f" },
	] as const;
	const counts = deriveCounts(gaps as any);
	assert.equal(counts.blocker, 1);
	assert.equal(counts.major, 2);
	assert.equal(counts.minor, 1);
});

test("verdictForScore: maps to correct bands", () => {
	assert.equal(verdictForScore(100), "PRODUCTION_READY");
	assert.equal(verdictForScore(80), "PRODUCTION_READY");
	assert.equal(verdictForScore(79), "NEEDS_WORK");
	assert.equal(verdictForScore(60), "NEEDS_WORK");
	assert.equal(verdictForScore(59), "SIGNIFICANT_GAPS");
	assert.equal(verdictForScore(40), "SIGNIFICANT_GAPS");
	assert.equal(verdictForScore(39), "NOT_IMPLEMENTED");
	assert.equal(verdictForScore(0), "NOT_IMPLEMENTED");
});

// ─── Catalog wiring ─────────────────────────────────────────────────────────

test("eval-review command is in catalog", () => {
	const entry = TOP_LEVEL_SUBCOMMANDS.find((c) => c.cmd === "eval-review");
	assert.ok(entry, "eval-review should be registered in TOP_LEVEL_SUBCOMMANDS");
	assert.ok(entry!.desc.includes("EVAL-REVIEW"));
});
