import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import process from "node:process";

import { validateSync } from "../../packages/codeflowmu-protocol/src/index.ts";
import {
  canonicalBytes,
  createTmpaGovernanceReader,
  stableValue,
  tmpaSha256Digest,
} from "../../packages/codeflowmu-runtime/src/tmpa/index.ts";
import { fixtureSourceDigest, tests } from "./product-fixtures.mjs";

type Verdict = "PASS" | "FAIL" | "PARTIAL" | "NOT RUN";

const root = path.resolve(import.meta.dirname, "../..");
const upstream = path.join(import.meta.dirname, "upstream");
const outputRoot = process.env.TMPA_PRODUCT_OUTPUT
  ? path.resolve(process.env.TMPA_PRODUCT_OUTPUT)
  : path.join(import.meta.dirname, "artifacts");
const executedAt = process.env.TMPA_EXECUTED_AT ?? new Date().toISOString();

function jsonFile(filePath: string): unknown {
  return JSON.parse(readFileSync(filePath, "utf8"));
}

function fileDigest(filePath: string): string {
  return tmpaSha256Digest(readFileSync(filePath));
}

function output(relativePath: string, value: unknown): string {
  const target = path.join(outputRoot, relativePath);
  mkdirSync(path.dirname(target), { recursive: true });
  const bytes = canonicalBytes(value);
  writeFileSync(target, bytes);
  return tmpaSha256Digest(bytes);
}

function getPointer(value: unknown, pointer: string): unknown {
  if (pointer === "" || pointer === "/") return value;
  return pointer
    .split("/")
    .slice(1)
    .reduce<unknown>((current, token) => {
      if (current === null || typeof current !== "object") return undefined;
      const key = token.replaceAll("~1", "/").replaceAll("~0", "~");
      return (current as Record<string, unknown>)[key];
    }, value);
}

function evaluate(operator: string, actual: unknown, expected: unknown): boolean {
  if (operator === "equals") {
    return JSON.stringify(stableValue(actual)) === JSON.stringify(stableValue(expected));
  }
  if (operator === "includes") return Array.isArray(actual) && actual.includes(expected);
  if (operator === "not_includes") {
    return Array.isArray(actual) && !actual.includes(expected);
  }
  if (operator === "all_equal") {
    return Array.isArray(actual) && actual.length > 0 && actual.every((item) => item === expected);
  }
  if (operator === "contains_all") {
    return (
      Array.isArray(actual) &&
      Array.isArray(expected) &&
      expected.every((item) => actual.includes(item))
    );
  }
  throw new Error(`Unknown assertion operator: ${operator}`);
}

function verdict(assertions: Array<{ mandatory: boolean; status: string }>): Verdict {
  const mandatory = assertions.filter((assertion) => assertion.mandatory);
  if (mandatory.some((assertion) => assertion.status === "failed")) return "FAIL";
  const passed = mandatory.filter((assertion) => assertion.status === "passed");
  if (passed.length === mandatory.length && mandatory.length > 0) return "PASS";
  if (passed.length > 0) return "PARTIAL";
  return "NOT RUN";
}

function aggregate(criteria: Array<{ verdict: Verdict }>): Verdict {
  const values = criteria.map((criterion) => criterion.verdict);
  if (values.includes("FAIL")) return "FAIL";
  if (values.includes("PARTIAL")) return "PARTIAL";
  if (values.includes("NOT RUN")) return "NOT RUN";
  return "PASS";
}

const lifecycleProfile = jsonFile(path.join(upstream, "profile.json"));
const profileValidation = validateSync("tmpa-s1.0-lifecycle-profile", lifecycleProfile);
if (!profileValidation.valid) {
  throw new Error(`Locked S1.0 lifecycle profile is invalid: ${JSON.stringify(profileValidation.errors)}`);
}

const schemaNames = [
  "governance-object.schema.json",
  "lifecycle-profile.schema.json",
  "reader-result.schema.json",
  "conformance-result.schema.json",
] as const;
const schemaDigests = Object.fromEntries(
  schemaNames.map((name) => [name, fileDigest(path.join(upstream, "schemas", name))]),
);
const inputBundle = {
  upstream_commit: "942cbb097eb3d662662f96a2269818ec9d7ca2ed",
  fixture_source_digest: fixtureSourceDigest(),
  locked_fixture_file_digest: fileDigest(path.join(upstream, "fixtures.mjs")),
  profile_digest: fileDigest(path.join(upstream, "profile.json")),
  canonicalization_profile_digest: fileDigest(
    path.join(upstream, "canonicalization-profile.json"),
  ),
  schema_digests: schemaDigests,
};
const inputBundleDigest = tmpaSha256Digest(canonicalBytes(inputBundle));
const productReader = createTmpaGovernanceReader({
  lifecycleProfile: lifecycleProfile as never,
  coreVersion: "S1.0",
  readerId: "codeflowmu-tmpa-s1.0-reader",
  readerVersion: "V1.8.0",
});

const criteria: Array<{
  id: string;
  verdict: Verdict;
  run_state: "completed";
  manifest_digest: string;
  result_digest: string;
}> = [];

for (const fixture of tests) {
  const inputInvocations: Array<
    Array<{
      source_id: string;
      media_type: string;
      byte_length: number;
      byte_digest: string;
      bytes_utf8: string;
    }>
  > = [];
  const manifest = {
    test_case_id: `S1.0-${fixture.id}-codeflowmu-v1.8.0-001`,
    criterion: fixture.id,
    core_version: "S1.0",
    implementation: { id: "codeflowmu", version: "V1.8.0" },
    upstream_commit: inputBundle.upstream_commit,
    input_bundle_digest: inputBundleDigest,
    assertions: fixture.assertions,
    product_reader_entrypoint:
      "packages/codeflowmu-runtime/src/tmpa/GovernanceReader.ts#readSync",
    reference_reader_called: false,
  };
  const manifestDigest = tmpaSha256Digest(canonicalBytes(manifest));
  const execution = fixture.run((sources) => {
    inputInvocations.push(
      sources.map((source) => ({
        source_id: source.source_id,
        media_type: source.media_type,
        byte_length: source.bytes.byteLength,
        byte_digest: tmpaSha256Digest(source.bytes),
        bytes_utf8: new TextDecoder("utf-8", { fatal: true }).decode(source.bytes),
      })),
    );
    return productReader.readSync(sources);
  });
  const assertions = fixture.assertions.map(
    (spec: {
      id: string;
      target: string;
      operator: string;
      expected: unknown;
      mandatory: boolean;
    }) => {
      const actual = getPointer(execution.context, spec.target);
      const passed = evaluate(spec.operator, actual, spec.expected);
      return {
        id: spec.id,
        mandatory: spec.mandatory,
        status: passed ? "passed" : "failed",
        target: spec.target,
        operator: spec.operator,
        actual,
        expected: spec.expected,
      };
    },
  );
  const criterionVerdict = verdict(assertions);
  const actual = {
    criterion: fixture.id,
    fixture_name: fixture.name,
    input_invocations: inputInvocations,
    assertions,
    reader_result_digest: tmpaSha256Digest(canonicalBytes(execution.result)),
    reader_result: execution.result,
  };
  const resultDigest = tmpaSha256Digest(canonicalBytes(actual));
  const record = {
    manifest,
    manifest_digest: manifestDigest,
    actual,
    verdict: criterionVerdict,
    result_digest: resultDigest,
  };
  output(path.join("criteria", `${fixture.id}.json`), record);
  criteria.push({
    id: fixture.id,
    verdict: criterionVerdict,
    run_state: "completed",
    manifest_digest: manifestDigest,
    result_digest: resultDigest,
  });
}

const conformanceResult = {
  core_version: "S1.0",
  implementation: { id: "codeflowmu", version: "V1.8.0" },
  input_bundle_digest: inputBundleDigest,
  criteria,
  aggregate_verdict: aggregate(criteria),
  evidence_level: "demonstrated",
};
const conformanceValidation = validateSync(
  "tmpa-s1.0-conformance-result",
  conformanceResult,
);
if (!conformanceValidation.valid) {
  throw new Error(
    `Product conformance result is invalid: ${JSON.stringify(conformanceValidation.errors)}`,
  );
}

const counts = Object.fromEntries(
  (["PASS", "FAIL", "PARTIAL", "NOT RUN"] as Verdict[]).map((name) => [
    name,
    criteria.filter((criterion) => criterion.verdict === name).length,
  ]),
);
const resultDigest = output("conformance-result.json", conformanceResult);
const summary = {
  executed_at: executedAt,
  command: "npm run test:tmpa:s1.0",
  working_directory: root.replaceAll("\\", "/"),
  implementation: conformanceResult.implementation,
  core_version: "S1.0",
  aggregate_verdict: conformanceResult.aggregate_verdict,
  counts,
  result_digest: resultDigest,
  input_bundle: inputBundle,
  input_bundle_digest: inputBundleDigest,
  product_reader_called: true,
  reference_reader_called: false,
  claim_boundary: "Author-run demonstrated product evidence; not independent certification.",
};
output("summary.json", summary);
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
if (conformanceResult.aggregate_verdict !== "PASS") process.exitCode = 1;
