/**
 * Quest-wiring regression tests (T012).
 *
 * Daily-quest pools used to contain 4 quest types with NO live completion path
 * (socialize, attend_class, work_hours, earn_money), so a player could be handed
 * a permanently-unwinnable quest. The fix:
 *   - WIRE `socialize` (fires via handlePerformActivity on the socialize activity)
 *   - WIRE `earn_money` ONLINE-ONLY by GROSS weekly income (PlayerSession week tick)
 *   - PRUNE `work_hours` + `attend_class` (no honest single-fire source)
 *
 * These tests prove the two wired types complete and the two pruned types are
 * gone, with every remaining template type backed by a live fire path.
 */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
  generateDailyQuests,
  getActiveQuests,
  getAllQuestTemplates,
  disableDatabaseStorage,
  clearAllQuests,
  type QuestType,
} from '../../../src/services/retention/dailyQuests.js';
import { Person } from '../../../src/models/Person.js';
import { Player } from '../../../src/models/Player.js';
import { PlayerSession } from '../../../src/game/PlayerSession.js';
import { handlePerformActivity } from '../../../src/handlers/performActivity.js';
import { computeWeeklyFinances } from '../../../src/game/finance.js';

// Quest types that have a live fire path after T012 (everything that can be
// issued must be in this set, or a player could get an unwinnable quest).
const LIVE_FIRE_TYPES: QuestType[] = [
  'talk_to_characters',
  'buy_item',
  'complete_activities',
  'spend_energy',
  'study',
  'increase_affinity',
  'go_on_date',
  'socialize',
  'earn_money',
];

class MockWebSocket {
  messages: string[] = [];
  readyState = 1; // OPEN
  send(data: string) {
    this.messages.push(data);
  }
  close() {
    this.readyState = 3;
  }
}

function callWeekTick(session: PlayerSession): void {
  (session as unknown as { processWeekTick: () => void }).processWeekTick();
}

/** Let fire-and-forget promises (e.g. the earn_money quest update) settle. */
async function flushMicrotasks(): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, 0));
}

describe('T012 quest wiring + prune', () => {
  beforeEach(() => {
    disableDatabaseStorage();
    clearAllQuests();
  });
  afterEach(() => {
    clearAllQuests();
    vi.restoreAllMocks();
  });

  // ---- PRUNE ----------------------------------------------------------------

  it('prunes work_hours and attend_class from the templates', () => {
    const types = getAllQuestTemplates().map((t) => t.type);
    expect(types).not.toContain('work_hours');
    expect(types).not.toContain('attend_class');
  });

  it('keeps every difficulty bucket non-empty and every template type wireable', () => {
    const templates = getAllQuestTemplates();
    for (const difficulty of ['easy', 'medium', 'hard'] as const) {
      const bucket = templates.filter((t) => t.difficulty === difficulty);
      expect(bucket.length).toBeGreaterThan(0);
    }
    // Every issuable template must have a live fire path (no unwinnable quests).
    for (const t of templates) {
      expect(LIVE_FIRE_TYPES).toContain(t.type);
    }
  });

  // ---- WIRE: socialize ------------------------------------------------------

  it('advances the socialize quest by exactly 1 per socialize activity', async () => {
    const character = new Person({
      id: 'char-soc',
      firstname: 'Soc',
      lastname: 'Tester',
      sex: 'Female',
      ageYears: 25,
      money: 1000,
      energy: 100,
      // Free evening slots so an immediate `socialize` activity can run
      // (handlePerformActivity needs a free slot at/after hourOfDay).
      dailyPlan: [
        { time: 18, location: 'home', title: 'Relax', name: 'home' },
        { time: 19, location: 'home', title: 'Relax', name: 'relax' },
        { time: 20, location: 'home', title: 'Relax', name: 'leisure' },
        { time: 21, location: 'home', title: 'Relax', name: 'play' },
      ] as never,
    } as never);
    const player = new Player({
      userId: 'socialize-quest-user',
      character,
      r: [],
      status: 'playing',
      date: '2024-06-15',
      hourOfDay: 10,
      minuteOfHour: 0,
    });
    const session = {
      player,
      sentMessages: [] as any[],
      send(m: any) {
        this.sentMessages.push(m);
      },
      sendPlayerObject() {
        this.sentMessages.push({ type: 'playerObject' });
      },
      async savePlayer() {},
    };

    // easy pool after prune = [talk_to_characters, buy_item, socialize].
    // floor(0.7 * 3) = 2 -> socialize.
    vi.spyOn(Math, 'random').mockReturnValue(0.7);
    await generateDailyQuests(player.userId);
    vi.spyOn(Math, 'random').mockRestore();

    const before = (await getActiveQuests(player.userId)).find((q) => q.questType === 'socialize');
    expect(before).toBeDefined();
    expect(before!.progress).toBe(0);

    await handlePerformActivity({ activityId: 'socialize' }, session as never, () => 0.5);

    const after = (await getActiveQuests(player.userId)).find((q) => q.questType === 'socialize');
    expect(after!.progress).toBe(1);
  });

  // ---- WIRE: earn_money (online weekly tick, GROSS not net) -----------------

  it('advances earn_money by GROSS weekly income on a week tick (not net)', async () => {
    const character = new Person({
      id: 'char-earn',
      firstname: 'Earn',
      lastname: 'Tester',
      sex: 'Male',
      ageYears: 30,
      occupation: 'engineer',
      salary: 2000,
      money: 100,
    } as never);
    const player = new Player({
      userId: 'earn-money-quest-user',
      character,
      status: 'playing',
      date: '2024-06-15',
    });

    // gross weekly income from the same source the wire uses.
    const gross = computeWeeklyFinances(player.c).grossIncome;
    expect(gross).toBeGreaterThan(0);

    // hard pool = [spend_energy, earn_money, increase_affinity].
    // floor(0.5 * 3) = 1 -> earn_money.
    vi.spyOn(Math, 'random').mockReturnValue(0.5);
    await generateDailyQuests(player.userId);
    vi.spyOn(Math, 'random').mockRestore();

    const earnQuest = (await getActiveQuests(player.userId)).find((q) => q.questType === 'earn_money');
    expect(earnQuest).toBeDefined();

    const ws = new MockWebSocket();
    const session = new PlayerSession(ws as unknown as never, player);
    vi.spyOn(session as unknown as { savePlayer: () => Promise<void> }, 'savePlayer').mockResolvedValue(
      undefined
    );

    const moneyBefore = player.c.money;
    callWeekTick(session);
    await flushMicrotasks();
    const netDelta = player.c.money - moneyBefore; // small, post rent/savings

    const after = (await getActiveQuests(player.userId)).find((q) => q.questType === 'earn_money');
    // Advanced by GROSS income (== required 500 for this fixture), NOT the much
    // smaller net money delta — proving the wire uses computeWeeklyFinances().grossIncome.
    expect(after!.progress).toBe(gross);
    expect(gross).toBeGreaterThan(netDelta);
  });
});
