import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, it } from "vitest";
import { buildOntologyPackage, loadOntologyPackage, verifyPackageDigest } from "./package.js";
import { initOntologyPackage } from "./scaffold.js";
import { renderReport, validateOntologyPackage } from "./validate.js";
import { OPENONTOLOGY_VERSION } from "./types.js";
import type { Claim, LoadedPackage } from "./types.js";

const NOW = "2026-07-26T00:00:00Z";
const dirs: string[] = [];

function scaffold(): { dir: string; pkg: LoadedPackage } {
  const dir = mkdtempSync(join(tmpdir(), "openontology-"));
  dirs.push(dir);
  initOntologyPackage(dir, { id: "test-ecosystem", now: NOW });
  return { dir, pkg: loadOntologyPackage(dir) };
}

afterAll(() => {
  for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
});

/** Clone the scaffold and mutate one claim, to isolate a single failure mode. */
function withClaims(pkg: LoadedPackage, mutate: (claims: Claim[]) => void): LoadedPackage {
  const copy = structuredClone(pkg);
  mutate(copy.data.claims);
  return copy;
}

describe("init + validate", () => {
  it("scaffolds a package that passes strict validation with no edits (R139)", () => {
    const { pkg } = scaffold();
    const report = validateOntologyPackage(pkg, { strict: true });
    const errors = report.findings.filter((f) => f.severity === "error");
    expect(errors).toEqual([]);
    expect(report.ok).toBe(true);
    expect(report.checked.entities).toBe(8);
    expect(report.checked.claims).toBe(14);
  });

  it("loads YAML manifest, YAML schema files, and NDJSON data together", () => {
    const { pkg } = scaffold();
    expect(pkg.manifest.kind).toBe("OntologyPackage");
    expect(pkg.schema.entityTypes.map((t) => t.id)).toEqual(["Person", "Organization", "Project"]);
    expect(pkg.data.sources).toHaveLength(2);
    expect(pkg.files.map((f) => f.path)).toContain("data/claims.ndjson");
  });

  it("loads a package from an explicit manifest path", () => {
    const { dir } = scaffold();
    const pkg = loadOntologyPackage(join(dir, "openontology.yaml"));
    expect(pkg.dir).toBe(dir);
    expect(pkg.manifest.id).toBe("test-ecosystem");
  });

  it("builds a deterministic digest that survives a rebuild", () => {
    const { pkg } = scaffold();
    const first = buildOntologyPackage(pkg);
    const second = buildOntologyPackage(loadOntologyPackage(structuredClone(pkg)));
    expect(first.digest).toBe(second.digest);
    expect(verifyPackageDigest(first).ok).toBe(true);
  });

  it("detects a tampered built package", () => {
    const { pkg } = scaffold();
    const built = buildOntologyPackage(pkg);
    built.files[0] = { ...built.files[0], digest: `sha256:${"0".repeat(64)}` };
    expect(verifyPackageDigest(built).ok).toBe(false);
  });

  it("reports a missing declared file instead of loading a partial package", () => {
    const { dir } = scaffold();
    rmSync(join(dir, "data/claims.ndjson"));
    expect(() => loadOntologyPackage(dir)).toThrow(/data.claims.*missing/);
  });
});

describe("graph validation", () => {
  it("rejects a claim whose subject type is outside the relationship domain", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      const claim = claims.find((c) => c.predicate === "worksOn") as Claim;
      claim.subject = "test:org:northwind"; // an Organization cannot worksOn
    });
    const report = validateOntologyPackage(broken);
    expect(report.ok).toBe(false);
    expect(report.findings.map((f) => f.code)).toContain("OO-G-DOMAIN");
  });

  it("rejects a claim whose object type is outside the relationship range", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      const claim = claims.find((c) => c.predicate === "worksOn") as Claim;
      claim.object = { entity: "test:org:northwind" };
    });
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-RANGE");
  });

  it("rejects a value object on a relationship predicate", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      const claim = claims.find((c) => c.predicate === "worksOn") as Claim;
      claim.object = { value: "ZK Prover" };
    });
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-OBJECT-KIND");
  });

  it("rejects a value that does not match its declared datatype", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      const claim = claims.find((c) => c.predicate === "homepage") as Claim;
      claim.object = { value: 42 };
    });
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-DATATYPE");
  });

  it("rejects dangling entity, source, and claim references", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      (claims[0] as Claim).subject = "test:person:nobody";
      (claims[1] as Claim).sources = ["test:source:missing"];
      (claims[2] as Claim).supersedes = "test:claim:9999";
    });
    const codes = validateOntologyPackage(broken).findings.map((f) => f.code);
    expect(codes.filter((c) => c === "OO-G-DANGLING-REF").length).toBeGreaterThanOrEqual(3);
  });

  it("rejects a claim with no source and no firstParty declaration (R54)", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      const claim = claims[0] as Claim;
      delete claim.sources;
      delete claim.firstParty;
    });
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-P-NO-SOURCE");
  });

  it("requires a runId on agent-authored claims (R55)", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      (claims[0] as Claim).assertedBy = "agent:research-mapper";
    });
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-P-NO-RUN");
  });

  it("requires derivedFrom on derived claims (R56)", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      (claims[0] as Claim).status = "derived";
    });
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-P-NO-DERIVATION");
  });

  it("rejects validTime.to before validTime.from", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      (claims[0] as Claim).validTime = { from: "2026-05-01T00:00:00Z", to: "2026-01-01T00:00:00Z" };
    });
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-TEMPORAL-ORDER");
  });

  it("catches duplicate ids", () => {
    const { pkg } = scaffold();
    const broken = structuredClone(pkg);
    broken.data.entities.push(structuredClone(broken.data.entities[0]));
    expect(validateOntologyPackage(broken).findings.map((f) => f.code)).toContain("OO-G-DUPLICATE-ID");
  });

  it("treats unknown predicates as warnings by default and errors in strict mode (R75)", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      (claims[0] as Claim).predicate = "notDeclared";
    });
    const lenient = validateOntologyPackage(broken);
    const strict = validateOntologyPackage(broken, { strict: true });
    expect(lenient.findings.find((f) => f.code === "OO-G-UNKNOWN-PREDICATE")?.severity).toBe("warning");
    expect(strict.findings.find((f) => f.code === "OO-G-UNKNOWN-PREDICATE")?.severity).toBe("error");
    expect(lenient.ok).toBe(true);
    expect(strict.ok).toBe(false);
  });

  it("flags a schema-invalid object with its path", () => {
    const { pkg } = scaffold();
    const broken = structuredClone(pkg);
    delete (broken.data.entities[0] as Partial<{ canonicalName: string }>).canonicalName;
    const report = validateOntologyPackage(broken);
    const finding = report.findings.find((f) => f.code === "OO-S-OBJECT");
    expect(finding).toBeDefined();
    expect(finding?.path).toMatch(/^\/0/);
  });
});

describe("provenance policy", () => {
  it("raises a policy finding for an over-long excerpt", () => {
    const { pkg } = scaffold();
    const withEvidence = structuredClone(pkg);
    withEvidence.data.evidence.push({
      openontology: OPENONTOLOGY_VERSION,
      kind: "Evidence",
      id: "test:evidence:1",
      source: "test:source:team-page",
      selector: { type: "line-range", start: 1, end: 2 },
      excerpt: "x".repeat(600)
    });
    const report = validateOntologyPackage(withEvidence, { maxExcerptLength: 500 });
    const finding = report.findings.find((f) => f.code === "OO-P-EXCERPT-LENGTH");
    expect(finding?.severity).toBe("policy");
    // A policy finding is not an error: the package still validates.
    expect(report.ok).toBe(true);
  });

  it("rejects public evidence drawn from a private source", () => {
    const { pkg } = scaffold();
    const copy = structuredClone(pkg);
    copy.data.sources[0].visibility = "private";
    copy.data.evidence.push({
      openontology: OPENONTOLOGY_VERSION,
      kind: "Evidence",
      id: "test:evidence:2",
      source: copy.data.sources[0].id,
      selector: { type: "whole-document" },
      visibility: "public"
    });
    expect(validateOntologyPackage(copy).findings.map((f) => f.code)).toContain("OO-P-VISIBILITY");
  });
});

describe("constraints", () => {
  it("reports a violation of a declared required-predicate constraint", () => {
    const { pkg } = scaffold();
    const broken = structuredClone(pkg);
    broken.data.claims = broken.data.claims.filter((c) => c.predicate !== "homepage");
    const report = validateOntologyPackage(broken);
    const violations = report.findings.filter((f) => f.code === "OO-C-REQUIRED-PREDICATE");
    expect(violations).toHaveLength(3);
    expect(violations[0].severity).toBe("warning");
  });
});

describe("report rendering", () => {
  it("renders text, json, yaml, and markdown (R71)", () => {
    const { pkg } = scaffold();
    const report = validateOntologyPackage(pkg);
    expect(renderReport(report, "text")).toContain("OpenOntology package is valid.");
    expect(JSON.parse(renderReport(report, "json")).ok).toBe(true);
    expect(renderReport(report, "yaml")).toContain("ok: true");
    expect(renderReport(report, "markdown")).toContain("# Validation passed");
  });

  it("gives every finding a stable code and a remediation hint where known", () => {
    const { pkg } = scaffold();
    const broken = withClaims(pkg, (claims) => {
      delete (claims[0] as Claim).sources;
    });
    const finding = validateOntologyPackage(broken).findings.find((f) => f.code === "OO-P-NO-SOURCE");
    expect(finding?.hint).toMatch(/firstParty/);
  });
});
