import { describe, test } from "node:test";
import assert from "node:assert/strict";

import {
  KORTFORM_WEIGHTS,
  LONG_FORM_WEIGHTS,
  composite,
  band,
  triage,
  requiredDimensions,
  scoreEnvelope,
  capForActionability,
} from "../src/score.js";

const r1 = (x: number) => Math.round(x * 10) / 10;
const sum = (o: Record<string, number>) => Object.values(o).reduce((a, b) => a + b, 0);

describe("trends scorer (RE-R1 / B2)", () => {
  // SSOT: references/trend-scoring-modes.md — weights, band thresholds, and action
  // strings are pinned here so silent drift in any of them fails loudly.
  describe("pinned weights (SSOT)", () => {
    test("kortform weights match the SSOT and sum to 1.0", () => {
      assert.equal(KORTFORM_WEIGHTS.pillar, 0.3);
      assert.equal(KORTFORM_WEIGHTS.audience, 0.25);
      assert.equal(KORTFORM_WEIGHTS.timing, 0.2);
      assert.equal(KORTFORM_WEIGHTS.angle, 0.15);
      assert.equal(KORTFORM_WEIGHTS.authority, 0.1);
      assert.equal(r1(sum(KORTFORM_WEIGHTS)), 1.0);
    });

    test("long-form weights match the SSOT and sum to 1.0", () => {
      assert.equal(LONG_FORM_WEIGHTS.pillar, 0.3);
      assert.equal(LONG_FORM_WEIGHTS.depth, 0.25);
      assert.equal(LONG_FORM_WEIGHTS.angle, 0.2);
      assert.equal(LONG_FORM_WEIGHTS.authority, 0.15);
      assert.equal(LONG_FORM_WEIGHTS.currency, 0.1);
      assert.equal(r1(sum(LONG_FORM_WEIGHTS)), 1.0);
    });
  });

  describe("composite", () => {
    test("all-tens -> exactly 10.0 (proves Sigma weights = 1.0) for both modes", () => {
      const kort = { pillar: 10, audience: 10, timing: 10, angle: 10, authority: 10 };
      const long = { pillar: 10, depth: 10, angle: 10, authority: 10, currency: 10 };
      assert.equal(composite(kort, "kortform"), 10.0);
      assert.equal(composite(long, "long-form"), 10.0);
    });

    test("asymmetric golden vector {10,8,6,4,2} in dimension order -> 7.0 for both modes", () => {
      // 10*.30 + 8*.25 + 6*.20 + 4*.15 + 2*.10 = 3.0 + 2.0 + 1.2 + 0.6 + 0.2 = 7.0
      const kort = { pillar: 10, audience: 8, timing: 6, angle: 4, authority: 2 };
      const long = { pillar: 10, depth: 8, angle: 6, authority: 4, currency: 2 };
      assert.equal(composite(kort, "kortform"), 7.0);
      assert.equal(composite(long, "long-form"), 7.0);
    });

    test("a dimension below 1 throws", () => {
      const kort = { pillar: 0, audience: 5, timing: 5, angle: 5, authority: 5 };
      assert.throws(() => composite(kort, "kortform"), /range|1.*10|dimension/i);
    });

    test("a dimension above 10 throws", () => {
      const kort = { pillar: 11, audience: 5, timing: 5, angle: 5, authority: 5 };
      assert.throws(() => composite(kort, "kortform"));
    });

    test("a missing dimension throws", () => {
      const kort = { pillar: 5, audience: 5, timing: 5, angle: 5 }; // authority missing
      assert.throws(() => composite(kort as Record<string, number>, "kortform"));
    });
  });

  describe("band — boundaries + exact SSOT action strings", () => {
    test("8.0 -> Immediate", () => {
      const b = band(8.0);
      assert.equal(b.priority, "Immediate");
      assert.equal(b.kortformAction, "Draft within 24h");
      assert.equal(b.longformAction, "Promote to the edition backlog now");
    });

    test("6.0 -> High", () => {
      const b = band(6.0);
      assert.equal(b.priority, "High");
      assert.equal(b.kortformAction, "Publish within 48–72h");
      assert.equal(b.longformAction, "Strong edition candidate — schedule it");
    });

    test("4.0 -> Medium", () => {
      const b = band(4.0);
      assert.equal(b.priority, "Medium");
      assert.equal(b.kortformAction, "Add to this week's calendar");
      assert.equal(b.longformAction, "Hold as a backlog candidate, revisit");
    });

    test("2.0 -> Low", () => {
      const b = band(2.0);
      assert.equal(b.priority, "Low");
      assert.equal(b.kortformAction, "Note, skip for now");
      assert.equal(b.longformAction, "Park unless the angle sharpens");
    });

    test("below 2.0 -> Skip", () => {
      const b = band(1.9);
      assert.equal(b.priority, "Skip");
      assert.equal(b.kortformAction, "Off positioning");
      assert.equal(b.longformAction, "Off positioning");
    });

    test("just below a boundary lands in the lower band (7.9->High, 5.9->Medium, 3.9->Low)", () => {
      assert.equal(band(7.9).priority, "High");
      assert.equal(band(5.9).priority, "Medium");
      assert.equal(band(3.9).priority, "Low");
    });
  });

  describe("triage", () => {
    const candidates = [
      { id: "low", scores: { pillar: 2, audience: 2, timing: 2, angle: 2, authority: 2 } }, // 2.0
      { id: "high", scores: { pillar: 8, audience: 8, timing: 8, angle: 8, authority: 8 } }, // 8.0
      { id: "mid", scores: { pillar: 5, audience: 5, timing: 5, angle: 5, authority: 5 } }, // 5.0
      { id: "below", scores: { pillar: 3, audience: 3, timing: 3, angle: 3, authority: 3 } }, // 3.0
    ];

    test("keeps composite >= threshold, drops below, ranks kept composite-desc, annotates", () => {
      const { kept, dropped } = triage(candidates, { mode: "kortform", threshold: 4.0 });
      assert.deepEqual(
        kept.map((k) => k.id),
        ["high", "mid"],
      ); // 8.0, 5.0 desc; both >= 4.0
      assert.deepEqual(
        dropped.map((d) => d.id).sort(),
        ["below", "low"],
      ); // 3.0, 2.0 < 4.0
      assert.equal(kept[0].composite, 8.0);
      assert.equal(kept[0].band.priority, "Immediate");
      assert.equal(kept[1].composite, 5.0);
      assert.equal(kept[1].band.priority, "Medium");
    });

    test("threshold is inclusive (composite == threshold is kept)", () => {
      const { kept } = triage(candidates, { mode: "kortform", threshold: 5.0 });
      assert.deepEqual(
        kept.map((k) => k.id),
        ["high", "mid"],
      ); // mid == 5.0 kept
    });

    test("an empty candidate list -> empty kept/dropped", () => {
      const { kept, dropped } = triage([], { mode: "kortform", threshold: 4.0 });
      assert.deepEqual(kept, []);
      assert.deepEqual(dropped, []);
    });
  });

  describe("requiredDimensions (RE-R3a / SC1)", () => {
    test("kortform -> the five keys in SSOT weight order", () => {
      assert.deepEqual(requiredDimensions("kortform"), ["pillar", "audience", "timing", "angle", "authority"]);
    });

    test("long-form -> the five keys in SSOT weight order", () => {
      assert.deepEqual(requiredDimensions("long-form"), ["pillar", "depth", "angle", "authority", "currency"]);
    });

    test("order is pinned to the SSOT weight literals (a silent reorder fails)", () => {
      assert.deepEqual(requiredDimensions("kortform"), Object.keys(KORTFORM_WEIGHTS));
      assert.deepEqual(requiredDimensions("long-form"), Object.keys(LONG_FORM_WEIGHTS));
    });
  });

  describe("scoreEnvelope (RE-R3a / SC1)", () => {
    test("composes composite()+band() — composite/priority equal the existing functions (one owner)", () => {
      const dims = { pillar: 8, audience: 7, timing: 9, angle: 6, authority: 5 };
      const env = scoreEnvelope("kortform", dims);
      assert.equal(env.mode, "kortform");
      assert.deepEqual(env.dimensions, dims);
      assert.equal(env.composite, composite(dims, "kortform"));
      assert.equal(env.priority, band(composite(dims, "kortform")).priority);
    });

    test("long-form envelope composes the long-form composite/band", () => {
      const dims = { pillar: 9, depth: 8, angle: 7, authority: 6, currency: 5 };
      const env = scoreEnvelope("long-form", dims);
      assert.equal(env.composite, composite(dims, "long-form"));
      assert.equal(env.priority, band(composite(dims, "long-form")).priority);
    });

    test("a bad dimension makes scoreEnvelope throw (via composite — defense-in-depth contract)", () => {
      const dims = { pillar: 8, audience: 7, timing: 99, angle: 6, authority: 5 };
      assert.throws(() => scoreEnvelope("kortform", dims));
    });
  });

  // MR-F7 (N7): a candidate whose reader-grip is explicitly NOT formulated cannot occupy the
  // top `Immediate` band regardless of composite — the gate caps it to at most `High`. The
  // reader-side verdict lives in the N6 `actionability` field; the gate only enforces it, it
  // never invents a value. Absent actionability is left uncapped (additive-optional, no migration).
  describe("capForActionability — F7 band-cap gate (MR-F7)", () => {
    const immediateDims = { pillar: 8, audience: 8, timing: 8, angle: 8, authority: 8 }; // composite 8.0 -> Immediate
    const highDims = { pillar: 6, audience: 6, timing: 6, angle: 6, authority: 6 }; // composite 6.0 -> High
    const mediumDims = { pillar: 5, audience: 5, timing: 5, angle: 5, authority: 5 }; // composite 5.0 -> Medium

    test("Immediate + no reader-grip (formulated:false) -> capped to High, regardless of composite", () => {
      const env = scoreEnvelope("kortform", immediateDims);
      assert.equal(env.priority, "Immediate"); // precondition: composite 8.0 lands Immediate
      assert.equal(capForActionability(env, { formulated: false }).priority, "High");
    });

    test("Immediate + reader-grip formulated (true) -> stays Immediate", () => {
      const env = scoreEnvelope("kortform", immediateDims);
      assert.equal(capForActionability(env, { formulated: true }).priority, "Immediate");
    });

    test("Immediate + actionability undefined -> uncapped (legacy / un-annotated records not penalized)", () => {
      const env = scoreEnvelope("kortform", immediateDims);
      assert.equal(capForActionability(env, undefined).priority, "Immediate");
    });

    test("already High + formulated:false -> stays High (caps TO High, never below)", () => {
      const env = scoreEnvelope("kortform", highDims);
      assert.equal(env.priority, "High");
      assert.equal(capForActionability(env, { formulated: false }).priority, "High");
    });

    test("Medium + formulated:false -> untouched (gate bites only the top band)", () => {
      const env = scoreEnvelope("kortform", mediumDims);
      assert.equal(env.priority, "Medium");
      assert.equal(capForActionability(env, { formulated: false }).priority, "Medium");
    });

    test("caps priority only — composite/dimensions/mode preserved (score stays etterprøvbar)", () => {
      const dims = { pillar: 9, depth: 9, angle: 9, authority: 9, currency: 9 }; // 9.0 -> Immediate
      const env = scoreEnvelope("long-form", dims);
      const gated = capForActionability(env, { formulated: false });
      assert.equal(gated.priority, "High");
      assert.equal(gated.composite, env.composite); // real score unchanged
      assert.deepEqual(gated.dimensions, env.dimensions);
      assert.equal(gated.mode, env.mode);
    });

    test("does not mutate the input envelope (returns a fresh object)", () => {
      const env = scoreEnvelope("kortform", immediateDims);
      capForActionability(env, { formulated: false });
      assert.equal(env.priority, "Immediate"); // original untouched
    });
  });
});
