import { describe, expect, it } from 'vitest';
import type { GameData, ResearchState, TechDef } from '../contracts';
import { gateFor, indexTech, isUnlocked, techProgress, techStatus } from './research';

const TECH: TechDef[] = [
  { id: 'disc-artifact-bottling', era: 'disc', cost: { 'analog-pack': 10 }, unlocks: ['artifact-bottler'] },
  { id: 'reel-silver-seams', era: 'reel', cost: { 'analog-pack': 5 }, unlocks: ['extract-silver-frames'] },
  { id: 'stream-software-decoding', era: 'stream', cost: { 'spatial-pack': 4, 'analog-pack': 2 }, unlocks: ['software-decoder', 'mosh-reactor'] },
];

const DATA = { items: [], machines: [], recipes: [], tech: TECH, commissions: [] } as unknown as GameData;
const index = indexTech(DATA);
const research = (over: Partial<ResearchState> = {}): ResearchState =>
  ({ active: null, progress: {}, unlocked: [], ...over });

describe('the v4 gating rule', () => {
  it('locks an id that any tech claims', () => {
    expect(isUnlocked(index, research(), 'artifact-bottler')).toBe(false);
    expect(isUnlocked(index, research(), 'mosh-reactor')).toBe(false);
  });

  it('leaves ids referenced by no tech always available', () => {
    // "ids referenced by no tech are always available" — belts must never padlock.
    expect(isUnlocked(index, research(), 'belt')).toBe(true);
    expect(isUnlocked(index, research(), 'quantizer')).toBe(true);
  });

  it('unlocks every id a completed tech claims', () => {
    const r = research({ unlocked: ['stream-software-decoding'] });
    expect(isUnlocked(index, r, 'software-decoder')).toBe(true);
    expect(isUnlocked(index, r, 'mosh-reactor')).toBe(true);
    expect(isUnlocked(index, r, 'artifact-bottler')).toBe(false); // different tech
  });

  it('treats absent research as nothing unlocked, not as everything unlocked', () => {
    // SIM has no research yet; the safe reading of the rule is that gates hold.
    expect(isUnlocked(index, undefined, 'artifact-bottler')).toBe(false);
    expect(isUnlocked(index, undefined, 'belt')).toBe(true);
  });

  it('names the tech a locked machine is waiting on, for the REQUIRES tooltip', () => {
    expect(gateFor(index, research(), 'artifact-bottler')?.id).toBe('disc-artifact-bottling');
    expect(gateFor(index, research(), 'belt')).toBeNull();
    expect(gateFor(index, research({ unlocked: ['disc-artifact-bottling'] }), 'artifact-bottler'))
      .toBeNull();
  });
});

describe('indexTech', () => {
  it('groups techs by era in era order, skipping empty eras', () => {
    expect(indexTech(DATA).byEra.map((g) => g.era)).toEqual(['reel', 'disc', 'stream']);
  });

  it('keeps an era DATA invents rather than dropping its techs', () => {
    const odd = { ...DATA, tech: [...TECH, { id: 'x', era: 'vhs', cost: {}, unlocks: [] } as unknown as TechDef] };
    const eras = indexTech(odd as GameData).byEra.map((g) => g.era);
    expect(eras).toContain('vhs');
    expect(eras.at(-1)).toBe('vhs'); // last, not jumping the queue
  });
});

describe('techStatus / techProgress', () => {
  it('reports done, active and available', () => {
    expect(techStatus(research({ unlocked: ['reel-silver-seams'] }), TECH[1])).toBe('done');
    expect(techStatus(research({ active: 'reel-silver-seams' }), TECH[1])).toBe('active');
    expect(techStatus(research(), TECH[1])).toBe('available');
  });

  it('is 1 when done and 0 when not being worked on', () => {
    expect(techProgress(research({ unlocked: ['reel-silver-seams'] }), TECH[1])).toBe(1);
    expect(techProgress(research({ active: 'disc-artifact-bottling' }), TECH[1])).toBe(0);
  });

  it('averages across every pack the tech costs', () => {
    // stream-software-decoding wants 4 spatial + 2 analog = 6 packs; 3 delivered = 0.5
    const r = research({ active: 'stream-software-decoding', progress: { 'spatial-pack': 2, 'analog-pack': 1 } });
    expect(techProgress(r, TECH[2])).toBeCloseTo(0.5);
  });

  it('never exceeds 1 when the sim over-delivers a pack', () => {
    const r = research({ active: 'reel-silver-seams', progress: { 'analog-pack': 99 } });
    expect(techProgress(r, TECH[1])).toBe(1);
  });

  it('is 0 with no research state at all', () => {
    expect(techProgress(undefined, TECH[1])).toBe(0);
    expect(techStatus(undefined, TECH[1])).toBe('available');
  });
});
