// @vitest-environment happy-dom
import { describe, expect, it, vi } from 'vitest';
import type { Command, GameData, ResearchState, SimSnapshot, UIBus } from '../contracts';
import { createTechPanel } from './techpanel';

const DATA: GameData = {
  items: [
    { id: 'analog-pack', name: 'ANALOG PACK', codex: '', tier: 2, color: '#7fff9f' },
    { id: 'spatial-pack', name: 'SPATIAL PACK', codex: '', tier: 2, color: '#7fb0ff' },
  ],
  machines: [
    { id: 'software-decoder', name: 'SOFTWARE DECODER', codex: '', kind: 'power', footprint: { x: 2, y: 2 }, recipes: [], powerDraw: 0, asset: 'x' },
  ],
  recipes: [],
  tech: [
    { id: 'reel-silver-seams', era: 'reel', cost: { 'analog-pack': 4 }, unlocks: ['extract-silver'] },
    { id: 'stream-software-decoding', era: 'stream', cost: { 'analog-pack': 2, 'spatial-pack': 2 }, unlocks: ['software-decoder'] },
  ],
  commissions: [],
};

function mount(research?: ResearchState) {
  document.body.innerHTML = '';
  const sent: Command[] = [];
  const bus: UIBus = {
    dispatch: (c) => sent.push(c),
    selectedBuild: () => null,
    pickTile: () => null,
  };
  const panel = createTechPanel(DATA, bus);
  document.body.append(panel.el);
  const snap = { research } as unknown as SimSnapshot;
  return { panel, sent, snap };
}

const nodes = () => Array.from(document.querySelectorAll('.fk-tech-node')) as HTMLElement[];
const nodeFor = (text: string) => nodes().find((n) => n.textContent?.includes(text))!;

describe('tech panel', () => {
  it('starts shut and toggles on T', () => {
    const { panel } = mount();
    expect(panel.isOpen()).toBe(false);
    panel.toggle();
    expect(panel.isOpen()).toBe(true);
    expect(panel.el.classList.contains('is-open')).toBe(true);
    panel.close();
    expect(panel.isOpen()).toBe(false);
  });

  it('renders one node per tech, grouped under era headings in era order', () => {
    const { panel } = mount();
    panel.toggle();
    expect(nodes()).toHaveLength(2);
    const eras = Array.from(document.querySelectorAll('.fk-tech-era')).map((e) => e.textContent);
    expect(eras).toEqual(['REEL', 'STREAM']);
  });

  it('strips the era prefix from the node name — the heading already says it', () => {
    const { panel } = mount();
    panel.toggle();
    expect(nodeFor('SILVER SEAMS').querySelector('.fk-tech-name')!.textContent)
      .toBe('SILVER SEAMS');
  });

  it('says what a tech unlocks, in machine names not ids', () => {
    const { panel } = mount();
    panel.toggle();
    expect(nodeFor('SOFTWARE DECODING').getAttribute('title'))
      .toBe('UNLOCKS: SOFTWARE DECODER');
  });

  it('dispatches setResearch when a node is picked', () => {
    const { panel, sent, snap } = mount({ active: null, progress: {}, unlocked: [] });
    panel.toggle();
    panel.update(snap);
    nodeFor('SILVER SEAMS').dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(sent).toEqual([{ kind: 'setResearch', tech: 'reel-silver-seams' }]);
  });

  it('stands the active tech down when it is picked again', () => {
    const { panel, sent, snap } = mount({ active: 'reel-silver-seams', progress: {}, unlocked: [] });
    panel.toggle();
    panel.update(snap);
    nodeFor('SILVER SEAMS').dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(sent).toEqual([{ kind: 'setResearch', tech: null }]);
  });

  it('ignores clicks on a ratified tech', () => {
    const { panel, sent, snap } = mount({ active: null, progress: {}, unlocked: ['reel-silver-seams'] });
    panel.toggle();
    panel.update(snap);
    nodeFor('SILVER SEAMS').dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(sent).toEqual([]);
  });

  it('marks active and ratified nodes, and shows pack progress on the active one', () => {
    const { panel, snap } = mount({
      active: 'stream-software-decoding',
      progress: { 'analog-pack': 1 },
      unlocked: ['reel-silver-seams'],
    });
    panel.toggle();
    panel.update(snap);

    const done = nodeFor('SILVER SEAMS');
    expect(done.classList.contains('is-done')).toBe(true);
    expect(done.querySelector('.fk-tech-status')!.textContent).toBe('RATIFIED');

    const active = nodeFor('SOFTWARE DECODING');
    expect(active.classList.contains('is-active')).toBe(true);
    expect(active.querySelector('.fk-tech-status')!.textContent).toBe('IN COMMITTEE');
    // 1 of 4 packs delivered
    expect((active.querySelector('.fk-bar-f') as HTMLElement).style.width).toBe('25%');
    expect(active.textContent).toContain('1/2'); // analog packs
  });

  it('says RESEARCH OFFLINE when the sim reports no research state', () => {
    const { panel, snap } = mount(undefined);
    panel.toggle();
    panel.update(snap);
    expect(document.querySelector('.fk-tech-offline')!.classList.contains('is-on')).toBe(true);
  });

  it('drops the offline notice once research is running', () => {
    const { panel } = mount();
    panel.toggle();
    panel.update({ research: { active: null, progress: {}, unlocked: [] } } as unknown as SimSnapshot);
    expect(document.querySelector('.fk-tech-offline')!.classList.contains('is-on')).toBe(false);
  });

  it('does no work while shut', () => {
    const { panel, snap } = mount({ active: 'reel-silver-seams', progress: {}, unlocked: [] });
    const spy = vi.spyOn(document, 'querySelector');
    panel.update(snap); // closed
    spy.mockRestore();
    expect(nodeFor('SILVER SEAMS').classList.contains('is-active')).toBe(false);
  });
});

describe('about / credits footer', () => {
  const about = () => document.querySelector('.fk-credits-btn') as HTMLElement;
  const credits = () => document.querySelector('.fk-credits') as HTMLElement;

  it('offers an ABOUT entry that starts collapsed', () => {
    const { panel } = mount();
    panel.toggle();
    expect(about().textContent).toBe('ABOUT');
    expect(credits().classList.contains('is-on')).toBe(false);
  });

  it('reveals repo, codex and the standing synthetic-voice disclosure slot', () => {
    const { panel } = mount();
    panel.toggle();
    about().dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(credits().classList.contains('is-on')).toBe(true);
    const txt = credits().textContent!;
    expect(txt).toContain('gitea.partly.party/monster/glytch');
    expect(txt).toContain('docs/FKTRY_LORE.md');
    // The slot is a reservation, not a filled credit yet.
    expect(document.querySelector('.fk-credits-slot')!.textContent)
      .toContain('SYNTHETIC VOICE CREDIT');
  });

  it('toggles back shut on a second click', () => {
    const { panel } = mount();
    panel.toggle();
    about().dispatchEvent(new MouseEvent('click', { bubbles: true }));
    about().dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(credits().classList.contains('is-on')).toBe(false);
  });
});
