/**
 * Unit tests driving shipped pure logic modules (PRD tables/rules).
 */
import { describe, it, expect, beforeEach } from 'vitest';
import { AFU_DAILY_LINES, getAfuLine } from './afuLines';
import { TASKS, getTask } from './tasks';
import {
  HOMOPHONE_PAIRS,
  corruptText,
  faceTierForNpc,
  petalCount,
  showBlackSeeds,
  stageFromHappiness,
  stageParams,
} from './corruption';
import { evaluateEnding } from './endings';
import { CLUES, C9_LINES, C9_DAYS } from './clues';
import {
  CLUE_AWARENESS,
  FORCED_CELEBRATION_MS,
  FORCED_CELEBRATION_RESTORE,
  HAPPINESS_START,
  OVERFLOW_POOL_MAX,
  SADNESS_AWARENESS,
  SADNESS_HAPPINESS,
  T14_FADE_START_MS,
  T14_STAND_MS,
} from './constants';
import { Director } from '../systems/CorruptionDirector';
import { gameEvents } from '../systems/EventBus';

describe('§5 Afu daily lines verbatim', () => {
  it('has 7 lines matching PRD', () => {
    expect(AFU_DAILY_LINES).toHaveLength(7);
    expect(getAfuLine(1)).toBe('早上好呀！今天的幸福任务贴在板上啦。祝你开心！');
    expect(getAfuLine(2)).toBe('早上好呀！新的一天，新的幸福。祝你开心！');
    expect(getAfuLine(3)).toBe('早上好！今天要把幸福装得满满的哦。要开心！');
    expect(getAfuLine(4)).toBe(getAfuLine(1)); // D4 = D1 character-identical
    expect(getAfuLine(5)).toBe('早上好。今天是自由日，你可以自由地选择幸福。要开心哦。');
    expect(getAfuLine(6)).toBe('明天就是庆典了。大家都会到场，大家都会微笑。你必须开心。');
    expect(getAfuLine(7)).toBe('庆典开始了。来吧。');
  });
});

describe('§7 task rewards', () => {
  it('matches exact happiness rewards', () => {
    expect(getTask('T1').happinessReward).toBe(15);
    expect(getTask('T2').happinessReward).toBe(10);
    expect(getTask('T3').happinessReward).toBe(10);
    expect(getTask('T4').happinessReward).toBe(15);
    expect(getTask('T5').happinessReward).toBe(10);
    expect(getTask('T6').happinessReward).toBe(10);
    expect(getTask('T7').happinessReward).toBe(15);
    expect(getTask('T8').happinessReward).toBe(15);
    expect(getTask('T9').happinessReward).toBe(10);
    expect(getTask('T10').happinessReward).toBe(10);
    expect(getTask('T11').happinessReward).toBe(10);
    expect(getTask('T12').happinessReward).toBe(10);
    expect(getTask('T13').happinessReward).toBe(10);
    expect(getTask('T14').happinessReward).toBe(15);
    expect(getTask('T15').happinessReward).toBe(0);
  });

  it('D4 T8–T10 descriptions are character-identical to D1 T1–T3', () => {
    expect(getTask('T8').description).toBe(getTask('T1').description);
    expect(getTask('T9').description).toBe(getTask('T2').description);
    expect(getTask('T10').description).toBe(getTask('T3').description);
  });

  it('has exactly 15 tasks', () => {
    expect(TASKS).toHaveLength(15);
  });
});

describe('§6.4 stage mapping', () => {
  it('maps happiness 10/40/70/90/100 → S0–S4', () => {
    expect(stageFromHappiness(10)).toBe('S0');
    expect(stageFromHappiness(40)).toBe('S1');
    expect(stageFromHappiness(70)).toBe('S2');
    expect(stageFromHappiness(90)).toBe('S3');
    expect(stageFromHappiness(100)).toBe('S4');
  });

  it('stage params match matrix saturation / rate / lowpass / bird', () => {
    expect(stageParams('S0').saturation).toBe(0.9);
    expect(stageParams('S1').saturation).toBe(1.0);
    expect(stageParams('S2').saturation).toBe(1.15);
    expect(stageParams('S2').playbackRate).toBe(0.97);
    expect(stageParams('S3').saturation).toBe(1.3);
    expect(stageParams('S3').playbackRate).toBe(0.92);
    expect(stageParams('S3').lowpassHz).toBe(8000);
    expect(stageParams('S4').saturation).toBe(1.4);
    expect(stageParams('S4').playbackRate).toBe(0.84);
    expect(stageParams('S4').lowpassHz).toBe(4000);
    expect(stageParams('S4').birdMute).toBe(true);
    expect(stageParams('S4').showBlackSeeds).toBe(true);
  });
});

describe('petals and black seeds', () => {
  it('computes petal counts and seeds at 100', () => {
    expect(petalCount(0)).toBe(0);
    expect(petalCount(10)).toBe(1);
    expect(petalCount(30)).toBe(3);
    expect(petalCount(100)).toBe(12);
    expect(showBlackSeeds(99)).toBe(false);
    expect(showBlackSeeds(100)).toBe(true);
  });
});

describe('§6.5 text corruption', () => {
  it('has all 10 homophone pairs', () => {
    expect(HOMOPHONE_PAIRS).toHaveLength(10);
    expect(HOMOPHONE_PAIRS).toEqual(
      expect.arrayContaining([
        ['幸福', '辛福'],
        ['开心', '开欣'],
        ['美好', '美妤'],
        ['大家', '大伽'],
        ['微笑', '微效'],
        ['明天', '名天'],
        ['甜', '恬'],
        ['阳光', '央光'],
        ['朋友', '棚友'],
        ['永远', '泳远'],
      ]),
    );
  });

  it('keeps source clean at S0/S1 and corrupts at S3+ for mimi', () => {
    const src = '幸福开心美好大家微笑明天甜阳光朋友永远！';
    expect(corruptText(src, 'S0', { isMimi: true })).toBe(src);
    const s4 = corruptText(src, 'S4', { isMimi: true, sentenceIndex: 0 });
    expect(s4).not.toBe(src);
    expect(s4.includes('辛福') || s4.includes('开欣')).toBe(true);
  });
});

describe('NPC face tiers with offsets', () => {
  it('mimi earlier, yuanyuan later, immune always smile', () => {
    // mimi offset −10: flat at 71, off at 90
    expect(faceTierForNpc(70, -10)).toBe('smile');
    expect(faceTierForNpc(71, -10)).toBe('flat');
    expect(faceTierForNpc(89, -10)).toBe('flat');
    expect(faceTierForNpc(90, -10)).toBe('off');
    // yuanyuan +15: flat at 96, off at 115 (never at ≤100)
    expect(faceTierForNpc(95, 15)).toBe('smile');
    expect(faceTierForNpc(96, 15)).toBe('flat');
    expect(faceTierForNpc(100, 15)).toBe('flat');
    expect(faceTierForNpc(100, 'immune')).toBe('smile');
  });
});

describe('§9 ending priority', () => {
  it('E4 when low happiness streak and 3 escapes', () => {
    expect(
      evaluateEnding({
        awareness: 0,
        happiness: 5,
        clueCount: 0,
        lowHappinessStreakD5to7: true,
        celebrationEscapes: 3,
        choseFogWall: false,
      }),
    ).toBe('E4');
  });

  it('E3 when awareness≥70 clues≥6 and fog wall', () => {
    expect(
      evaluateEnding({
        awareness: 70,
        happiness: 50,
        clueCount: 6,
        lowHappinessStreakD5to7: false,
        celebrationEscapes: 0,
        choseFogWall: true,
      }),
    ).toBe('E3');
  });

  it('E2 for awareness 30–69', () => {
    expect(
      evaluateEnding({
        awareness: 40,
        happiness: 50,
        clueCount: 2,
        lowHappinessStreakD5to7: false,
        celebrationEscapes: 0,
        choseFogWall: false,
      }),
    ).toBe('E2');
  });

  it('E1 for awareness≤29 and happiness≥90', () => {
    expect(
      evaluateEnding({
        awareness: 10,
        happiness: 95,
        clueCount: 0,
        lowHappinessStreakD5to7: false,
        celebrationEscapes: 0,
        choseFogWall: false,
      }),
    ).toBe('E1');
  });
});

describe('§8 clues', () => {
  it('has 9 clues and C9 three steps', () => {
    expect(CLUES).toHaveLength(9);
    expect(C9_LINES).toEqual([
      '湖水一直很平静',
      '面包一直是那个味道',
      '孩子，今天是第几个今天？',
    ]);
    expect(C9_DAYS).toEqual([2, 4, 6]);
  });
});

describe('CorruptionDirector shipped API', () => {
  beforeEach(() => {
    Director.resetSession('测试员');
  });

  it('starts at happiness 30 awareness 0', () => {
    expect(Director.happiness).toBe(HAPPINESS_START);
    expect(Director.awareness).toBe(0);
    expect(Director.playerName).toBe('测试员');
  });

  it('applies task rewards via completeTask', () => {
    Director.setDay(1);
    Director.completeTask('T1');
    expect(Director.happiness).toBe(HAPPINESS_START + 15);
    expect(Director.completedTasks.has('T1')).toBe(true);
  });

  it('sadness deltas −5 happiness +3 awareness', () => {
    const h = Director.happiness;
    const a = Director.awareness;
    Director.applySadness('idle');
    expect(Director.happiness).toBe(h + SADNESS_HAPPINESS);
    expect(Director.awareness).toBe(a + SADNESS_AWARENESS);
  });

  it('clue adds +8 awareness and never decreases', () => {
    Director.day = 2;
    Director.discoverClue('C1');
    expect(Director.awareness).toBe(CLUE_AWARENESS);
    Director.setAwareness(5); // should not lower
    expect(Director.awareness).toBe(CLUE_AWARENESS);
  });

  it('overflow pool at 100 triggers after 15', () => {
    Director.setHappiness(100);
    let flashes = 0;
    const handler = () => {
      flashes++;
    };
    gameEvents.on('negativeFlash', handler);
    for (let i = 0; i < OVERFLOW_POOL_MAX; i++) {
      Director.addHappiness(-1);
    }
    expect(Director.happiness).toBe(100);
    expect(flashes).toBe(1);
    gameEvents.off('negativeFlash', handler);
  });

  it('forced celebration after 2 min below 30', () => {
    let t = 1_000_000;
    Director.nowMs = () => t;
    Director.setHappiness(20);
    expect(Director.lowHappinessSinceMs).toBe(t);
    t += FORCED_CELEBRATION_MS;
    Director.tick(0);
    expect(Director.forcedCelebrationActive).toBe(true);
    expect(Director.happiness).toBe(FORCED_CELEBRATION_RESTORE);
    Director.escapeForcedCelebration();
    expect(Director.celebrationEscapes).toBe(1);
  });

  it('well/fog gated before D5', () => {
    Director.setDay(3);
    expect(Director.canInteractWellOrFog()).toBe(false);
    Director.setDay(5);
    expect(Director.canInteractWellOrFog()).toBe(true);
  });

  it('empty name defaults to 小满', () => {
    Director.resetSession('   ');
    expect(Director.playerName).toBe('小满');
  });

  it('T14 timing constants', () => {
    expect(T14_STAND_MS).toBe(60000);
    expect(T14_FADE_START_MS).toBe(40000);
  });

  it('D4 task complete same rewards as D1', () => {
    Director.setDay(4);
    const h0 = Director.happiness;
    Director.completeTask('T8');
    expect(Director.happiness).toBe(h0 + 15);
  });

  it('getStageParams via director matches happiness bands', () => {
    for (const [h, stage] of [
      [10, 'S0'],
      [40, 'S1'],
      [70, 'S2'],
      [90, 'S3'],
      [100, 'S4'],
    ] as const) {
      Director.setHappiness(h);
      expect(Director.stage).toBe(stage);
      expect(Director.getStageParams().stage).toBe(stage);
    }
  });

  it('force endings', () => {
    Director.forceEnding('E3');
    expect(Director.endingId).toBe('E3');
    expect(Director.ended).toBe(true);
  });
});
