/**
 * Tests for the traceability coverage computation.
 *
 * These run over fixture strings rather than the live docs/requirements.md, so
 * their meaning does not drift as requirements are added.
 *
 * Background: the CI gate divided traced-requirement counts by hardcoded
 * denominators (UR/39, IR/24, DR/48, JA/3, total 114) that had fallen out of
 * date, reporting 158% coverage and making the 50% threshold unreachable. These
 * tests pin the parsing and arithmetic that replace those literals.
 *
 * @req-test: UT-089 - Requirement definitions parsed from requirements.md
 * @req-test: UT-090 - Coverage is the intersection of traced and defined IDs
 */

import { describe, it, expect } from "vitest";
import { countDefinedRequirements, computeCoverage } from "./extract-traces";

describe("countDefinedRequirements", () => {
  it("counts a well-formed table row as a defined requirement", () => {
    const md = `
| ID | Requirement | Priority | Status |
|----|-------------|----------|--------|
| UR-001 | Run the app on multiple platforms | High | In Progress |
| UR-002 | Access media when online or offline | High | Done |
`;
    const defined = countDefinedRequirements(md);
    expect(defined.UR).toBe(2);
    expect(defined.DR).toBe(0);
  });

  it("does not count IDs that appear only in the Traces To column", () => {
    // The bug this rule avoids: a naive grep for /DR-\d{3}/ over the whole file
    // counts DR-001 here as "defined", inflating the denominator with IDs that
    // are merely referenced.
    const md = `
| DR-001 | Player state machine | Player | UR-005 | Done |
| DR-002 | MediaItem struct | Player | UR-003, UR-004 | Done |
`;
    const defined = countDefinedRequirements(md);
    expect(defined.DR).toBe(2);
    // UR-005/UR-003/UR-004 are referenced, never defined here.
    expect(defined.UR).toBe(0);
  });

  it("does not count IDs mentioned in prose", () => {
    const md = `
Some prose explaining that UR-005 relates to DR-001 and JA-002.

| UR-005 | Control media playback | High | Done |
`;
    const defined = countDefinedRequirements(md);
    expect(defined.UR).toBe(1);
    expect(defined.DR).toBe(0);
    expect(defined.JA).toBe(0);
  });

  it("deduplicates an ID listed in both the spec table and the traceability matrix", () => {
    // requirements.md lists every UR twice: once in §1 (definition) and again in
    // §3 (traceability matrix), both as a leading table cell. Counting rows
    // instead of unique IDs double-counts the UR denominator (121 vs 61).
    const md = `
| UR-005 | Control media playback | High | Done |
| UR-006 | Browse the library | High | Done |

### Traceability Matrix

| UR-005 | - | DR-001, DR-005, DR-009 |
| UR-006 | - | DR-012 |
`;
    const defined = countDefinedRequirements(md);
    expect(defined.UR).toBe(2);
  });

  it("collects the defined ID set, not just counts", () => {
    const md = `
| UR-001 | A | High | Done |
| DR-050 | B | Player | UR-001 | Done |
`;
    const defined = countDefinedRequirements(md);
    expect(defined.ids.has("UR-001")).toBe(true);
    expect(defined.ids.has("DR-050")).toBe(true);
    expect(defined.ids.has("UR-999")).toBe(false);
  });
});

describe("computeCoverage", () => {
  const defined = {
    UR: 2,
    IR: 0,
    DR: 2,
    JA: 0,
    total: 4,
    ids: new Set(["UR-001", "UR-002", "DR-001", "DR-002"]),
  };

  it("computes coverage as traced ∩ defined over defined", () => {
    const traced = ["UR-001", "DR-001"];
    const cov = computeCoverage(traced, defined);
    expect(cov.covered).toBe(2);
    expect(cov.total).toBe(4);
    expect(cov.percent).toBe(50);
  });

  it("does not let a traced-but-undefined ID inflate the numerator", () => {
    // This is how a ratio exceeds 100%: a TRACES comment naming a typo'd or
    // deleted requirement counted as covered.
    const traced = ["UR-001", "DR-001", "DR-097"];
    const cov = computeCoverage(traced, defined);
    expect(cov.covered).toBe(2);
    expect(cov.percent).toBe(50);
  });

  it("reports traced-but-undefined IDs as orphaned so they get fixed", () => {
    const traced = ["UR-001", "DR-097", "JA-404"];
    const cov = computeCoverage(traced, defined);
    expect(cov.orphaned).toEqual(["DR-097", "JA-404"]);
  });

  it("has no orphans when every traced ID is defined", () => {
    const cov = computeCoverage(["UR-001", "UR-002"], defined);
    expect(cov.orphaned).toEqual([]);
  });

  it("ignores UT/IT test IDs entirely — they are a separate taxonomy", () => {
    // UT/IT are defined in §4 of requirements.md, not among the four
    // requirement types. Treating them as orphans buries real typos in ~60
    // lines of noise, and counting them would corrupt the ratio.
    const cov = computeCoverage(["UR-001", "UT-088", "IT-017"], defined);
    expect(cov.orphaned).toEqual([]);
    expect(cov.covered).toBe(1);
  });

  it("reports 0% rather than dividing by zero for an empty trace set", () => {
    const cov = computeCoverage([], defined);
    expect(cov.covered).toBe(0);
    expect(cov.percent).toBe(0);
  });

  it("reports 0% rather than NaN when nothing is defined", () => {
    const empty = { UR: 0, IR: 0, DR: 0, JA: 0, total: 0, ids: new Set<string>() };
    const cov = computeCoverage([], empty);
    expect(cov.percent).toBe(0);
    expect(Number.isNaN(cov.percent)).toBe(false);
  });

  it("reports exactly 100% when all defined requirements are traced, never above", () => {
    const traced = ["UR-001", "UR-002", "DR-001", "DR-002"];
    const cov = computeCoverage(traced, defined);
    expect(cov.percent).toBe(100);
  });

  it("ignores duplicate traced IDs", () => {
    const traced = ["UR-001", "UR-001", "UR-001"];
    const cov = computeCoverage(traced, defined);
    expect(cov.covered).toBe(1);
  });
});

describe("live requirements.md", () => {
  it("parses the real file to the counts the CI gate must use", () => {
    // Guards the specific regression: CI hardcoded UR/39, IR/24, DR/48, JA/3
    // (total 114) while the real file had grown to 211. Update these numbers
    // deliberately when requirements are added — that edit is the signal the
    // denominator is live rather than frozen.
    const fs = require("fs");
    const path = require("path");
    // import.meta.dir is Bun-only; derive from import.meta.url under vitest.
    const here = path.dirname(new URL(import.meta.url).pathname);
    const md = fs.readFileSync(
      path.resolve(here, "../docs/requirements.md"),
      "utf-8"
    );
    const defined = countDefinedRequirements(md);

    expect(defined.UR).toBe(71);
    expect(defined.IR).toBe(32);
    expect(defined.DR).toBe(150);
    expect(defined.JA).toBe(35);
    expect(defined.total).toBe(288);
  });
});
