// goAML XML serializer — structural conformance + fail-closed (no-fabrication) [DIRECT-DEV].
//
// The serializer (server/lib/fincrime/goaml-xml-serializer.ts) turns the STR-grounded GoamlFormB into
// Uganda goAML XML. It targets Uganda's DOCUMENTED goAML node structure (the real machine-readable Uganda
// XSD is portal-gated and was not obtainable at build time), so this is a STRUCTURAL-conformance test:
// XSD byte-validation against the actual Uganda XSD is PENDING and explicitly asserted as such here — the
// test does NOT claim XSD-validated. It DOES prove the load-bearing properties: well-formed XML, the goAML
// activity-report node structure, injection-safe escaping, and the never-fabricate fail-closed behavior on
// the opaque (civil-identity-deferred) subject.
//
// No DB — runs standalone: `npx tsx --test tests/goaml-serializer.test.ts`.

import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { renderGoamlFormB, getGoamlDeploymentConfig, GoamlFormBRenderError, type FormBSource } from "../server/lib/fincrime/goaml-formb.js";
import { serializeGoamlXml, UGANDA_GOAML_SCHEMA } from "../server/lib/fincrime/goaml-xml-serializer.js";

function source(overrides: Partial<FormBSource> = {}): FormBSource {
  return {
    subjectRef: "OPAQUE-REF-7F3A", subjectRefKind: "INTERNAL_CASE_SUBJECT",
    sourceType: "KYT_ALERT", disposition: "STR_RECOMMENDED", dispositionedAt: new Date("2036-01-02T00:00:00.000Z"),
    strFiled: false, strReportId: "STR-9001", strStatus: "PREPARED",
    // narrative deliberately carries XML-special chars to prove injection-safe escaping
    narrative: 'Structuring & layering <observed> via "smurfing"; subject\'s pattern matched <rule#7>.',
    strCreatedAt: new Date("2036-01-01T00:00:00.000Z"), fiaReference: null, filedAt: null, acknowledgedAt: null,
    preparedByRole: "fincrime_analyst", approverRole: "mlco", approvedAt: new Date("2036-01-02T00:00:00.000Z"),
    ...overrides,
  };
}

// Stack-based well-formedness check over the emitter's tag grammar (no attributes/self-closing except the
// XML declaration). Proves balanced, correctly-nested tags.
function isWellFormed(xml: string): boolean {
  const body = xml.replace(/^<\?xml[^?]*\?>\s*/, "");
  const stack: string[] = [];
  const re = /<(\/?)([a-z_]+)>/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(body))) {
    const [, slash, name] = m;
    if (slash) { if (stack.pop() !== name) return false; }
    else stack.push(name);
  }
  return stack.length === 0;
}

describe("goAML XML serializer — structural conformance + fail-closed", () => {
  const formB = renderGoamlFormB(source(), getGoamlDeploymentConfig());
  const result = serializeGoamlXml(formB);

  test("emits well-formed XML with the goAML activity-report node structure", () => {
    assert.ok(result.xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>'), "XML declaration");
    assert.ok(isWellFormed(result.xml), "tags must be balanced + correctly nested");
    for (const node of ["report", "rentity_id", "submission_code", "report_code", "entity_reference",
      "submission_date", "reporting_person", "reason", "report_indicators", "indicator",
      "activity", "report_parties", "report_party", "report_party_type"]) {
      assert.ok(result.xml.includes(`<${node}>`), `must contain <${node}>`);
    }
    assert.ok(result.xml.includes("<report_code>STR</report_code>"), "report_code = STR");
  });

  test("injection-safe: XML-special chars in the narrative are escaped, never raw", () => {
    assert.ok(result.xml.includes("&amp;") && result.xml.includes("&lt;observed&gt;") && result.xml.includes("&quot;smurfing&quot;"),
      "special chars must be entity-escaped");
    // the reason value must not contain a raw unescaped '<observed>' breaking the markup
    assert.ok(!result.xml.includes("<observed>"), "no raw unescaped markup from the narrative");
  });

  test("FAIL-CLOSED: opaque subject is NOT given a fabricated civil identity; the gap is surfaced", () => {
    // The opaque reference is emitted structurally...
    assert.ok(result.xml.includes("OPAQUE_EXTERNAL_REFERENCE") && result.xml.includes("OPAQUE-REF-7F3A"),
      "the opaque reference is emitted");
    // ...but NO civil identity is invented for the subject: the only first_name/last_name in the doc are
    // the registered MLCO reporting person's (AEGIS/MLCO) — never a fabricated name for the subject.
    const firstNames = (result.xml.match(/<first_name>([^<]*)<\/first_name>/g) || []);
    assert.equal(firstNames.length, 1, "exactly one first_name (the reporting MLCO), none fabricated for the subject");
    assert.ok(result.xml.includes("<first_name>AEGIS</first_name>"), "the one first_name is the reporting person's");
    // and the verdict surfaces the unresolved-identity gap + is NOT submittable
    assert.equal(result.submittable, false, "must NOT be submittable while civil identity is unresolved");
    assert.ok(result.gaps.some((g) => /civil identity is unresolved/i.test(g)), "must surface the civil-identity gap");
    assert.ok(result.gaps.some((g) => /NOT fabricated/i.test(g)), "must state the identity was not fabricated");
  });

  test("honest XSD status: validation is explicitly PENDING the real Uganda XSD (not claimed validated)", () => {
    assert.equal(result.xsdValidation, "PENDING_ACTUAL_UGANDA_XSD");
    assert.equal(UGANDA_GOAML_SCHEMA.xsdValidation, "PENDING_ACTUAL_UGANDA_XSD");
    assert.match(result.schemaTarget, /Uganda/i);
  });

  test("never-fabricate upstream: an unmappable disposition throws, it does not invent a report_code", () => {
    assert.throws(
      () => renderGoamlFormB(source({ disposition: "NO_ACTION" }), getGoamlDeploymentConfig()),
      GoamlFormBRenderError,
      "an unmapped disposition must STOP/SURFACE, never fabricate a goAML report_code",
    );
  });
});
