/* ============================================================
   Session 66 — THE PRICE LAYER: token layer + the five honesty states.

   Two things are locked here:
     1. The TOKEN LAYER is additive and complete — the design bundle's values
        exist in globals.css and match HANDOFF.md byte-for-byte.
     2. The LAWS are enforceable, not folklore — the verdict function is the
        single place "is this value?" is answered, and it can never say VALUE
        on a juiced price.
   ============================================================ */

const fs = require('fs');
const path = require('path');

const ROOT = path.join(__dirname, '..', '..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');

const CSS = read('web/src/app/globals.css');
const HANDOFF = read('specs/design-reference/HANDOFF.md');

const vs = require('../../web/src/lib/valueState');
const backendValue = require('../../src/config/valueEngine');

// ── 1. TOKEN LAYER — the design's values, present and correct ────────────
describe('token layer — HANDOFF values resolve in globals.css', () => {
  // Each entry: [css custom property, expected hex, why it exists]
  const TOKENS = [
    ['--bg-0', '#06060B'], // void
    ['--bg-1', '#0E0E14'], // card
    ['--bg-2', '#14141E'], // elevated
    ['--hairline', '#101018'], // row hairline
    ['--bg-deep', '#0A0A10'], // deep panel
    ['--border', '#1E1E2A'],
    ['--border-hi', '#2A2A38'],
    ['--text-0', '#F0F0F0'],
    ['--text-1', '#B8BCC8'],
    ['--text-2', '#707080'],
    ['--text-3', '#4a4a58'],
    ['--g-a', '#00d4a0'], // signal green
    ['--amber', '#ffb347'],
    ['--miss', '#FF4757'],
    ['--priced-out', '#8fb2de'], // NEW this session
  ];

  test.each(TOKENS)('%s is declared as %s', (name, hex) => {
    const m = CSS.match(new RegExp(`${name}\\s*:\\s*([^;]+);`));
    expect(m).toBeTruthy();
    expect(m[1].trim().toLowerCase()).toBe(hex.toLowerCase());
  });

  it('every hex above also appears in HANDOFF.md (design is the source)', () => {
    // The priced-out blue comes from the triplet file, not HANDOFF's token list.
    for (const [, hex] of TOKENS.filter(([n]) => n !== '--priced-out')) {
      expect(HANDOFF.toLowerCase()).toContain(hex.toLowerCase());
    }
  });

  it('grade colors follow the design: A green, B white, C grey, D/F red', () => {
    expect(CSS).toMatch(/--g-a:\s*#00d4a0/i);
    expect(CSS).toMatch(/--g-b:\s*#F0F0F0/i);
    expect(CSS).toMatch(/--g-c:\s*#B8BCC8/i);
    expect(CSS).toMatch(/--g-d:\s*#FF4757/i);
  });

  it('fonts: Inter for chrome, JetBrains Mono for ALL data', () => {
    expect(CSS).toMatch(/--sans:[^;]*Inter/);
    expect(CSS).toMatch(/--mono:[^;]*JetBrains Mono/);
  });

  it('the laws are written INTO the token layer, not left as folklore', () => {
    const block = CSS.slice(CSS.indexOf('SESSION 66 — PRICE-LAYER TOKENS'));
    expect(block).toMatch(/GREEN = EDGE ONLY/);
    expect(block).toMatch(/GLOW = A-TIER ONLY/);
    expect(block).toMatch(/AMBER = CAUTION/);
    expect(block).toMatch(/RED = MISS \/ NEGATIVE only/i);
    expect(block).toMatch(/BLUE = EDGE PRICED OUT/);
    expect(block).toMatch(/JETBRAINS MONO = ALL DATA/);
  });

  it('is ADDITIVE — the new block declares only new names', () => {
    // Bound the block to the Session-66 section itself: globals.css continues
    // past :root with a11y/media layers that legitimately re-declare tokens.
    const start = CSS.indexOf('SESSION 66 — PRICE-LAYER TOKENS');
    const block = CSS.slice(start, CSS.indexOf('\n}', start));
    const declared = [...block.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gim)].map((m) => m[1]);
    expect(declared.length).toBeGreaterThan(0);
    // None of the newly-declared names may collide with a pre-existing token.
    const before = CSS.slice(0, CSS.indexOf('SESSION 66 — PRICE-LAYER TOKENS'));
    for (const name of declared) {
      expect(before).not.toMatch(new RegExp(`^\\s*${name}\\s*:`, 'm'));
    }
  });
});

// ── 2. THE BAND — frontend law mirrors the backend engine ────────────────
describe('takeable band + EV threshold stay in sync with the backend', () => {
  it('mirrors src/config/valueEngine.js exactly', () => {
    expect(vs.TAKEABLE_ODDS_CEILING).toBe(backendValue.TAKEABLE_ODDS_CEILING);
    expect(vs.TAKEABLE_ODDS_MAX).toBe(backendValue.TAKEABLE_ODDS_MAX);
    expect(vs.VALUE_EV_THRESHOLD).toBe(backendValue.VALUE_EV_THRESHOLD);
  });

  it('agrees with the backend on isTakeable / isValue across the band', () => {
    const prices = [-400, -210, -161, -160, -110, 100, 200, 201, 500, null, undefined, ''];
    for (const p of prices) {
      expect(vs.isTakeable(p)).toBe(backendValue.isTakeable(p));
      for (const ev of [-58.7, 0, 1.9, 2, 11.7, 26.5]) {
        expect(vs.isValue(p, ev)).toBe(backendValue.isValue(p, ev));
      }
    }
  });

  it('a missing price is NEVER takeable (Number(null) === 0 guard)', () => {
    expect(vs.isTakeable(null)).toBe(false);
    expect(vs.isTakeable('')).toBe(false);
    expect(vs.isTakeable(undefined)).toBe(false);
  });
});

// ── 3. THE FIVE HONESTY STATES ───────────────────────────────────────────
describe('deriveValueState — the single verdict function', () => {
  const base = { book_odds: -120, fair_odds: -104, model_odds: -196, ev_pct: 21.4 };

  it('VALUE — real edge at a takeable price (the live Petey Halpin row)', () => {
    expect(vs.deriveValueState(base)).toBe(vs.STATES.VALUE);
  });

  it('PRICED OUT — positive EV at a juiced price (the live Josh Bell row)', () => {
    // book -210, +11.7% EV: the engine refuses to call this value, and so do we.
    const row = { book_odds: -210, fair_odds: -173, model_odds: -311, ev_pct: 11.7 };
    expect(vs.deriveValueState(row)).toBe(vs.STATES.PRICED_OUT);
  });

  it('NEVER calls raw positive EV "value" — the whole point of state 2', () => {
    for (const [book, ev] of [[-210, 11.7], [-190, 26.5], [-400, 60], [250, 30]]) {
      const st = vs.deriveValueState({ book_odds: book, fair_odds: -150, model_odds: -300, ev_pct: ev });
      expect(st).not.toBe(vs.STATES.VALUE);
      expect(st).toBe(vs.STATES.PRICED_OUT);
    }
  });

  it('EV below the threshold is NO EDGE even at a takeable price', () => {
    expect(vs.deriveValueState({ ...base, ev_pct: 0.2 })).toBe(vs.STATES.NO_EDGE);
    expect(vs.deriveValueState({ ...base, ev_pct: 1.99 })).toBe(vs.STATES.NO_EDGE);
    expect(vs.deriveValueState({ ...base, ev_pct: 2 })).toBe(vs.STATES.VALUE); // boundary
  });

  it('QUARANTINE — model leg withheld, book + fair still stand', () => {
    const row = { ...base, quarantine_reason: 'wrong_opponent_grade' };
    expect(vs.deriveValueState(row)).toBe(vs.STATES.QUARANTINE);
  });

  it('REFUSAL — no fair price we would defend', () => {
    expect(vs.deriveValueState({ ...base, fair_odds: null })).toBe(vs.STATES.REFUSAL);
    expect(vs.deriveValueState({ ...base, book_odds: null })).toBe(vs.STATES.REFUSAL);
    expect(vs.deriveValueState({ ...base, refused: true })).toBe(vs.STATES.REFUSAL);
    expect(vs.deriveValueState({})).toBe(vs.STATES.REFUSAL);
  });

  it('a missing model price or EV withholds the verdict — never asserts NO EDGE', () => {
    expect(vs.deriveValueState({ ...base, model_odds: null })).toBe(vs.STATES.QUARANTINE);
    expect(vs.deriveValueState({ ...base, ev_pct: null })).toBe(vs.STATES.QUARANTINE);
  });

  it('a LOCKED model leg is neither quarantine nor refusal', () => {
    const row = { ...base, model_price_locked: true };
    expect(vs.deriveValueState(row)).toBe(vs.STATES.NO_VERDICT_LOCKED);
  });
});

// ── 4. THE COLOR LAW ─────────────────────────────────────────────────────
describe('valueStateColor — green appears on exactly one state', () => {
  it('VALUE is the ONLY green', () => {
    expect(vs.valueStateColor(vs.STATES.VALUE)).toBe('var(--g-a)');
    const others = [
      vs.STATES.PRICED_OUT, vs.STATES.NO_EDGE, vs.STATES.QUARANTINE,
      vs.STATES.REFUSAL, vs.STATES.NO_VERDICT_LOCKED,
    ];
    for (const s of others) expect(vs.valueStateColor(s)).not.toBe('var(--g-a)');
  });

  it('PRICED OUT is the blue carve-out; NO EDGE is never red', () => {
    expect(vs.valueStateColor(vs.STATES.PRICED_OUT)).toBe('var(--priced-out)');
    // An honest "no" is the instrument working, not a loss.
    expect(vs.valueStateColor(vs.STATES.NO_EDGE)).not.toBe('var(--miss)');
  });

  it('QUARANTINE is amber (caution), per the token law', () => {
    expect(vs.valueStateColor(vs.STATES.QUARANTINE)).toBe('var(--amber)');
  });
});

// ── 5. HONEST ABSENCE ────────────────────────────────────────────────────
describe('never prints a number it cannot stand behind', () => {
  it('fmtOddsAmerican returns null for absence — never 0, never a placeholder', () => {
    expect(vs.fmtOddsAmerican(null)).toBeNull();
    expect(vs.fmtOddsAmerican('')).toBeNull();
    expect(vs.fmtOddsAmerican(undefined)).toBeNull();
    expect(vs.fmtOddsAmerican('abc')).toBeNull();
    expect(vs.fmtOddsAmerican(-210)).toBe('-210');
    expect(vs.fmtOddsAmerican(125)).toBe('+125');
  });

  it('modelVsFair is null unless BOTH legs are real', () => {
    expect(vs.modelVsFair(null, -110)).toBeNull();
    expect(vs.modelVsFair(-110, null)).toBeNull();
    expect(typeof vs.modelVsFair(98, 110)).toBe('number');
  });

  it('reproduces the design file\'s own worked examples EXACTLY', () => {
    // The design states these two figures; the formula was derived from them.
    // STATE 1: book +125 · fair +110 · model +98  → "+2.9% VS FAIR"
    expect(vs.modelVsFair(98, 110)).toBe(2.9);
    // STATE 3: book +118 · fair +104 · model +112 → "-1.8% VS FAIR"
    expect(vs.modelVsFair(112, 104)).toBe(-1.8);
  });

  it('compares MODEL to FAIR — not book to fair (the induction bug)', () => {
    // Petey Halpin, live: book -120 · fair -104 · model -196. Our price is far
    // shorter than fair, so the gap is strongly POSITIVE. Comparing the book
    // to fair instead produced -6.5% on a VALUE row — a visible contradiction.
    expect(vs.modelVsFair(-196, -104)).toBeGreaterThan(10);
  });
});

// ── 6. THE COMPONENT carries the rulings ─────────────────────────────────
describe('PriceTriplet component', () => {
  const src = read('web/src/components/vyndr/PriceTriplet.tsx');

  it('uses ONLY tokens — no literal hex anywhere', () => {
    const hexes = src.match(/#[0-9a-f]{3,8}\b/gi) || [];
    expect(hexes).toEqual([]);
  });

  it('does not re-derive the verdict — it renders deriveValueState', () => {
    expect(src).toContain('deriveValueState');
    expect(src).not.toMatch(/ev_pct\s*>=\s*2/);
  });

  it('FAIR is the amber hero and is never locked', () => {
    expect(src).toMatch(/label="FAIR"[\s\S]{0,200}var\(--amber\)/);
    expect(src).not.toMatch(/label="FAIR"[\s\S]{0,200}locked/);
  });

  it('only the MODEL leg is lockable / withheld', () => {
    expect(src).toMatch(/label="MODEL"[\s\S]{0,300}locked=\{modelLocked\}/);
  });

  it('carries no sample numbers from the design file', () => {
    // Strip comments first — the docblock names the design's sample values in
    // order to say they are a SPEC and must never ship as data.
    const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
    for (const sample of ['1,188', '1,115.5', '+125', '+110', '+98', '+130', '+116']) {
      expect(code).not.toContain(sample);
    }
  });

  it('REFUSAL renders no legs and no gauge', () => {
    const refusal = src.slice(src.indexOf('STATES.REFUSAL'));
    expect(refusal).toContain("CAN&rsquo;T PRICE THIS ONE");
  });
});

// ── 6b. CONSUMERS MUST FORWARD THE LOCK FLAG ─────────────────────────────
describe('a gated model leg never wears quarantine copy', () => {
  it('deriveValueState separates GATED from WITHHELD', () => {
    const gated = { book_odds: -153, fair_odds: -129, model_odds: null, ev_pct: null, model_price_locked: true };
    const withheld = { book_odds: -153, fair_odds: -129, model_odds: null, ev_pct: null };
    expect(vs.deriveValueState(gated)).toBe(vs.STATES.NO_VERDICT_LOCKED);
    expect(vs.deriveValueState(withheld)).toBe(vs.STATES.QUARANTINE);
  });

  it('LiveHeroProp forwards model_price_locked (live bug: it did not)', () => {
    // Without this the landing hero rendered "MODEL READ WITHHELD" — poison's
    // copy — to every anonymous visitor, when the real reason was the paywall.
    const hero = read('web/src/components/LiveHeroProp.tsx');
    expect(hero).toMatch(/model_price_locked: data\.model_price_locked/);
  });

  it('the grade adapter forwards it too', () => {
    const { buildPriceTriplet } = require('../../web/src/lib/gradeAdapter');
    const t = buildPriceTriplet({ book_odds: -153, fair_odds: -129, model_price_locked: true }).priceTriplet;
    expect(t.model_price_locked).toBe(true);
  });
});

// ── 7. THE SERVER GATE — fair is never the paywall ───────────────────────
describe('free-tier gate strips the model price at the wire', () => {
  const { applyTierGating } = require('../../src/utils/tierGating');
  const row = {
    grade: 'B', book_odds: -120, fair_odds: -104, model_odds: -196, ev_pct: 21.4,
  };

  it('free: model_odds is REMOVED, book + fair survive', () => {
    const out = applyTierGating(row, 'free');
    expect(out.model_odds).toBeUndefined();
    expect(out.book_odds).toBe(-120);
    expect(out.fair_odds).toBe(-104);
    expect(out.model_price_locked).toBe(true);
  });

  it('analyst + desk: the full triplet passes through', () => {
    for (const tier of ['analyst', 'desk']) {
      const out = applyTierGating(row, tier);
      expect(out.model_odds).toBe(-196);
      expect(out.model_price_locked).toBeUndefined();
    }
  });

  it('the adapter turns a stripped row into a LOCK, not an absent leg', () => {
    const { buildPriceTriplet } = require('../../web/src/lib/gradeAdapter');
    const gated = applyTierGating(row, 'free');
    const t = buildPriceTriplet(gated).priceTriplet;
    expect(t.model_odds).toBeNull();
    expect(t.model_price_locked).toBe(true);
    expect(vs.deriveValueState(t)).toBe(vs.STATES.NO_VERDICT_LOCKED);
  });

  it('no book/fair → no price story at all (section hidden, not empty)', () => {
    const { buildPriceTriplet } = require('../../web/src/lib/gradeAdapter');
    expect(buildPriceTriplet({ grade: 'B' })).toEqual({});
    expect(buildPriceTriplet({ book_odds: -120 })).toEqual({});
  });
});
