import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import { assumedSize, ownCost, cheapestCost } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js';

/* Geschätzte Größe bei fehlender Angabe (SPEC §9, D66): statt pauschal M wird
   aus den Teilpaketen geschätzt — mindestens die größte Größe der zählenden
   Kinder, ab drei Kindern dieser Größe eine Stufe mehr (Deckel XXL). Es zählen
   dieselben Kinder wie beim Größen-Konflikt (§5/D62), nur dass fehlende
   Kindgrößen rekursiv mitgeschätzt werden. */

const roots = txt => parse(txt).roots;
const t = (key, vars) => {
  let s = key;
  if(vars) for(const k in vars) s += ':' + vars[k];
  return s;
};
const opts = () => ({ t, showDiscarded: false, cheapPath: true, cheapSet: new Set() });

describe('assumedSize — Schätzung aus den Teilpaketen (D66)', () => {
  it('Blatt ohne Größe bleibt M (der alte D18-Rückfall)', () => {
    expect(assumedSize(roots('[ ] Blatt')[0])).toBe('M');
  });

  it('explizite Größe gewinnt immer', () => {
    const [n] = roots(`[ ] Eltern (S)
  - [ ] Kind (XL)`);
    expect(assumedSize(n)).toBe('S');
  });

  it('mindestens die größte Kindgröße', () => {
    const [n] = roots(`[ ] Eltern
  - [ ] A (L)
  - [ ] B (S)`);
    expect(assumedSize(n)).toBe('L');
  });

  it('zwei Kinder der größten Größe: noch keine Stufe mehr', () => {
    const [n] = roots(`[ ] Eltern
  - [ ] A (S)
  - [ ] B (S)`);
    expect(assumedSize(n)).toBe('S');
  });

  it('drei Kinder der größten Größe: eine Stufe mehr', () => {
    const [n] = roots(`[ ] Eltern
  - [ ] A (S)
  - [ ] B (S)
  - [ ] C (S)
  - [ ] D (XS)`);
    expect(assumedSize(n)).toBe('M');
  });

  it('Deckel XXL: drei XXL-Kinder bleiben XXL', () => {
    const [n] = roots(`[ ] Eltern
  - [ ] A (XXL)
  - [ ] B (XXL)
  - [ ] C (XXL)`);
    expect(assumedSize(n)).toBe('XXL');
  });

  it('Kinder ohne Größe werden rekursiv mitgeschätzt', () => {
    /* Das Kind hat selbst 3×M-Kinder ⇒ geschätzt L; der Elternknoten erbt L. */
    const [n] = roots(`[ ] Eltern
  - [ ] Kind
    - [ ] a (M)
    - [ ] b (M)
    - [ ] c (M)`);
    expect(assumedSize(n.children[0])).toBe('L');
    expect(assumedSize(n)).toBe('L');
  });

  it('drei größenlose Blätter zählen als 3×M ⇒ L', () => {
    const [n] = roots(`[ ] Eltern
  - [ ] a
  - [ ] b
  - [ ] c`);
    expect(assumedSize(n)).toBe('L');
  });

  it('optionale und verworfene Kinder zählen nicht', () => {
    const [n] = roots(`[ ] Eltern
  - [ ] A (S)
  + [ ] Zugabe (XXL)
  - [-] Weg (XXL)`);
    expect(assumedSize(n)).toBe('S');
  });

  it('nur optionale Kinder ⇒ M-Rückfall', () => {
    const [n] = roots(`[ ] Eltern
  + [ ] Zugabe (XL)`);
    expect(assumedSize(n)).toBe('M');
  });

  it('disjunktive Gruppe: die kleinste Alternative ist der Boden', () => {
    const [n] = roots(`[ ] Eltern
  | [ ] A (XL)
  | [ ] B (S)`);
    expect(assumedSize(n)).toBe('S');
  });

  it('disjunktiv mit getroffener Wahl (D61): die kleinste der realisierten', () => {
    const [n] = roots(`[ ] Eltern
  = [~] A (XL)
  = [ ] B (S)`);
    expect(assumedSize(n)).toBe('XL');
  });

  it('drei gleich große Alternativen bleiben ohne Stufe mehr — nur eine wird realisiert', () => {
    const [n] = roots(`[ ] Eltern
  | [ ] A (L)
  | [ ] B (L)
  | [ ] C (L)`);
    expect(assumedSize(n)).toBe('L');
  });
});

describe('die Schätzung zählt nur offene Kinder (D70)', () => {
  it('erledigte Kinder fallen heraus — geschätzt wird der Rest', () => {
    const [n] = roots(`[ ] Paket
  - [x] Großteil (L)
  - [ ] Rest (S)`);
    expect(assumedSize(n)).toBe('S');
  });

  it('die Stufe ab drei zählt nur die offenen', () => {
    const [n] = roots(`[ ] Paket
  - [x] A (S)
  - [x] B (S)
  - [ ] C (S)
  - [ ] D (S)`);
    expect(assumedSize(n)).toBe('S');
  });

  it('alles Benannte erledigt: XS — der eigene Rest, nicht der M-Rückfall', () => {
    /* Mit M-Rückfall ERHÖHTE das Fertigstellen des letzten Kindes den
       Preis von S auf M — die Schätzung muss monoton sinken. */
    const [n] = roots(`[ ] Paket
  - [x] A (S)
  - [x] B (S)`);
    expect(assumedSize(n)).toBe('XS');
    expect(ownCost(n)).toBe(1);
  });

  it('ein echtes Blatt ohne Kinder bleibt beim M-Rückfall', () => {
    expect(assumedSize(roots('[ ] Blatt')[0])).toBe('M');
  });

  it('disjunktiv: eine erledigte realisierte Alternative stellt die Gruppe fertig', () => {
    const [n] = roots(`[ ] Paket
  | [x] Gemacht (S)
  | [ ] Teuer (M)`);
    expect(assumedSize(n)).toBe('XS');
  });

  it('disjunktiv: erledigt neben angefangen — die Gruppe trägt nichts mehr bei', () => {
    /* Der Pfad wählte unter den realisierten die erledigte (Kosten 0, D61) —
       die Schätzung folgt derselben Wahl. */
    const [n] = roots(`[ ] Paket
  | [x] Fertig (L)
  | [~] Angefangen (S)`);
    expect(assumedSize(n)).toBe('XS');
  });

  it('eine ANGEGEBENE Größe bleibt trotz erledigter Kinder stehen (D69)', () => {
    const [n] = roots(`[ ] Paket (L)
  - [x] Großteil (L)`);
    expect(assumedSize(n)).toBe('L');
  });
});

describe('ownCost und Pfad rechnen mit der Schätzung', () => {
  it('ownCost eines größenlosen Elternknotens folgt der Schätzung', () => {
    const [n] = roots(`[ ] Eltern
  - [ ] Kind (XL)`);
    expect(ownCost(n)).toBe(ownCost(roots('[ ] X (XL)')[0]));
  });

  it('cheapestCost steigt gegenüber der alten M-Annahme', () => {
    const derived = cheapestCost(roots(`[ ] Eltern
  - [ ] Kind (XL)`)[0]);
    const stated = cheapestCost(roots(`[ ] Eltern (M)
  - [ ] Kind (XL)`)[0]);
    expect(derived).toBeGreaterThan(stated);
  });

  it('am erledigten Knoten wird weiterhin nichts angenommen (Kosten 0)', () => {
    const [n] = roots(`[x] Eltern
  - [x] Kind (XL)`);
    expect(ownCost(n)).toBe(0);
  });
});

describe('Badge zeigt die geschätzte Größe', () => {
  it('das invertierte Badge trägt die abgeleitete Größe statt M', () => {
    const { html } = renderTreeHtml(roots(`[ ] Eltern
  - [ ] Kind (XL)`), opts());
    expect(html).toContain('<span class="size implicit" aria-hidden="true">XL</span>');
  });

  it('Tooltip und aria-label nennen die geschätzte Größe', () => {
    const { html } = renderTreeHtml(roots(`[ ] Eltern
  - [ ] Kind (L)`), opts());
    expect(html).toContain('implicitSizeTooltip:L');
    expect(html).toContain('a11ySizeImplicit:L');
  });

  it('Blatt ohne Größe zeigt weiterhin M', () => {
    const { html } = renderTreeHtml(roots('[ ] Blatt'), opts());
    expect(html).toContain('<span class="size implicit" aria-hidden="true">M</span>');
  });
});
