// Lifetime simulator scenarios. Each test runs the harness with a different
// strategy or seed and asserts the game loop doesn't crash or violate invariants.
//
// When a scenario fails during Phase 1, its failure is added to
// docs/plans/2026-04-13-baolife-phase2-backlog.md as a row, then the scenario
// is temporarily skipped with a `// phase2:` reason comment. The goal is to
// collect bugs, not to silence them.

import { describe, expect, it, vi } from 'vitest';

// -----------------------------------------------------------------------------
// Mocks (same set as smoke.test.ts)
// -----------------------------------------------------------------------------

vi.mock('../../src/database/players.js', () => ({
  savePlayer: vi.fn(async () => undefined),
  saveConversation: vi.fn(async () => undefined),
  saveConversationMessage: vi.fn(async () => undefined),
  loadPlayer: vi.fn(async () => null),
  loadConversations: vi.fn(async () => []),
  loadPlayerConversations: vi.fn(async () => []),
  markConversationAsRead: vi.fn(async () => undefined),
  updateConnectionStatus: vi.fn(async () => undefined),
  ensureTables: vi.fn(async () => undefined),
  saveGameAsync: vi.fn(async () => undefined),
  loadGameAsync: vi.fn(async () => null),
  loadAllPlayerIds: vi.fn(async () => []),
  loadGames: vi.fn(async () => []),
}));

type MemInstance = {
  instanceId: string;
  eventId: string;
  playerId: string;
  status: 'pending' | 'answered' | 'resolved' | 'cancelled';
  createdAt: string;
  answeredAt?: string;
  resolvedAt?: string;
  selectedChoiceId?: string;
};
const memInstances: MemInstance[] = [];

vi.mock('../../src/database/eventInstances.js', () => ({
  createEventInstance: vi.fn(async (input: Omit<MemInstance, 'createdAt' | 'status'>) => {
    const inst: MemInstance = { ...input, createdAt: new Date().toISOString(), status: 'pending' };
    memInstances.push(inst);
    return inst;
  }),
  getPendingEventInstances: vi.fn(async (playerId: string) =>
    memInstances.filter((i) => i.playerId === playerId && i.status === 'pending')
  ),
  answerEventInstance: vi.fn(async (instanceId: string, choiceId: string) => {
    const inst = memInstances.find((i) => i.instanceId === instanceId);
    if (!inst) return null;
    inst.status = 'answered';
    inst.answeredAt = new Date().toISOString();
    inst.selectedChoiceId = choiceId;
    return inst;
  }),
  resolveEventInstance: vi.fn(async (instanceId: string) => {
    const inst = memInstances.find((i) => i.instanceId === instanceId);
    if (!inst) return null;
    inst.status = 'resolved';
    inst.resolvedAt = new Date().toISOString();
    return inst;
  }),
  ensureEventInstancesTable: vi.fn(async () => undefined),
}));

vi.mock('../../src/services/notifications/notificationManager.js', () => ({
  notifyRealtimeEvent: vi.fn(async () => undefined),
  queueRealtimeNotification: vi.fn(async () => undefined),
  clearThrottle: vi.fn(() => undefined),
  notificationManager: {},
}));

vi.mock('../../src/events/conversations/npc_initiative.js', () => ({
  checkNPCInitiatedMessages: vi.fn(async () => undefined),
  checkRelationshipAtRiskNudges: vi.fn(async () => []),
  clearNPCInitiativeState: vi.fn(() => undefined),
  isAppropriateHour: vi.fn(() => true),
  detectNPCTriggers: vi.fn(() => []),
}));

vi.mock('../../src/services/retention/integration.js', () => ({
  onJobObtained: vi.fn(async () => undefined),
  onPromotion: vi.fn(async () => undefined),
  onFired: vi.fn(async () => undefined),
  onMarriage: vi.fn(async () => undefined),
  onDating: vi.fn(async () => undefined),
  onChildBorn: vi.fn(async () => undefined),
  onFriendMade: vi.fn(async () => undefined),
  onBirthday: vi.fn(async () => undefined),
  onGraduation: vi.fn(async () => undefined),
}));

import { simulateLifetime, type SimulatorResult } from './lifetime-simulator.js';
import type { EventPromptEnvelope } from '../../src/events/v2/types.js';

// One in-game year in minutes — scenarios cap at a reasonable horizon so a
// single CI run stays under 60 seconds total.
const ONE_YEAR_MIN = 365 * 24 * 60;

// Summary helper so failure output is readable.
function summarize(r: SimulatorResult): string {
  return JSON.stringify(
    {
      seed: r.seed,
      minutes: r.minutesSimulated,
      finalAge: r.finalAge,
      errors: r.errors.slice(0, 3).map((e) => ({
        tick: e.tick,
        phase: e.phase,
        message: e.error.message,
      })),
      invariants: r.invariantViolations.slice(0, 5),
    },
    null,
    2
  );
}

describe('lifetime simulator scenarios', () => {
  describe('short runs (1 in-game year)', () => {
    it('default responder, seed 1, no errors', async () => {
      const r = await simulateLifetime({ seed: 1, maxMinutes: ONE_YEAR_MIN });
      expect(r.errors, `scenario output:\n${summarize(r)}`).toEqual([]);
    }, 30_000);

    it('answer-all-first responder, seed 1, no errors', async () => {
      const r = await simulateLifetime({
        seed: 1,
        maxMinutes: ONE_YEAR_MIN,
        questionResponder: (p: EventPromptEnvelope) => p.choices[0]?.choiceId ?? '',
      });
      expect(r.errors, summarize(r)).toEqual([]);
    }, 30_000);

    it('answer-all-last responder, seed 1, no errors', async () => {
      const r = await simulateLifetime({
        seed: 1,
        maxMinutes: ONE_YEAR_MIN,
        questionResponder: (p: EventPromptEnvelope) =>
          p.choices[p.choices.length - 1]?.choiceId ?? '',
      });
      expect(r.errors, summarize(r)).toEqual([]);
    }, 30_000);
  });

  describe('random seeds (short, 1 year each)', () => {
    const seeds = [2, 7, 13, 42, 101];
    for (const seed of seeds) {
      it(`seed ${seed} completes without error`, async () => {
        const r = await simulateLifetime({ seed, maxMinutes: ONE_YEAR_MIN });
        expect(r.errors, summarize(r)).toEqual([]);
      }, 30_000);
    }
  });

  describe('invariants', () => {
    it('one-year default run does not violate stat bounds or numeric invariants', async () => {
      const r = await simulateLifetime({ seed: 42, maxMinutes: ONE_YEAR_MIN });
      expect(
        r.invariantViolations,
        `Invariant violations (first 10):\n${JSON.stringify(
          r.invariantViolations.slice(0, 10),
          null,
          2
        )}`
      ).toEqual([]);
    }, 30_000);
  });

  describe('longer runs (10 in-game years)', () => {
    it('seed 1 survives a decade without error', async () => {
      const r = await simulateLifetime({ seed: 1, maxMinutes: 10 * ONE_YEAR_MIN });
      expect(r.errors, summarize(r)).toEqual([]);
      expect(r.invariantViolations, summarize(r)).toEqual([]);
    }, 60_000);
  });

  describe('behavioral: the game actually progresses', () => {
    // These tests assert that a passing run is doing meaningful work, not
    // silently no-opping. If they start failing, it means the simulator
    // harness is loading but the game loop is degenerate.
    it('character ages over a 5-year simulation', async () => {
      const r = await simulateLifetime({
        seed: 99,
        characterAge: 10,
        maxMinutes: 5 * ONE_YEAR_MIN,
      });
      expect(r.errors, summarize(r)).toEqual([]);
      // 5 years of simulation should advance ageYears by roughly 5. Allow slack
      // in case a birthday lands within the window.
      expect(r.finalAge, `finalAge was ${r.finalAge}; expected ~15`).toBeGreaterThanOrEqual(14);
      expect(r.finalAge).toBeLessThanOrEqual(16);
    }, 60_000);

    // Event firing diversity: a 1-year run with a 10-year-old must fire at
    // least 3 unique v2 events. This pins the 2026-04-13 fix for interactive
    // event dedup (markAskedQuestion in EventResponder) — if it regresses,
    // the same event fires every hour and unique count collapses to 1.
    it('1-year run fires at least 3 unique v2 events (no repeat-forever bug)', async () => {
      const r = await simulateLifetime({
        seed: 7,
        characterAge: 10,
        maxMinutes: ONE_YEAR_MIN,
      });
      expect(r.errors, summarize(r)).toEqual([]);
      expect(
        r.eventsFired.length,
        `Only ${r.eventsFired.length} unique event(s) fired in 1 year. ` +
          `If this is 1, the v2 engine regressed on interactive dedup. ` +
          `Events: ${JSON.stringify(r.eventsFired)}`
      ).toBeGreaterThanOrEqual(3);
    }, 60_000);

    // Minute updates: a 1-year run should send hundreds of thousands of 'u'
    // messages. If this drops to near zero, the game loop stopped ticking.
    it('1-year run sends many minute updates (loop is live)', async () => {
      const r = await simulateLifetime({
        seed: 13,
        maxMinutes: ONE_YEAR_MIN,
      });
      expect(r.errors, summarize(r)).toEqual([]);
      const uCount = r.messageTypeCounts.u ?? 0;
      expect(uCount, `'u' messages sent: ${uCount}`).toBeGreaterThan(100_000);
    }, 60_000);
  });

  describe('soak: 20 seeds × 2 years', () => {
    // Broader seed coverage to catch seed-specific issues the fixed seeds miss.
    // Each simulation is ~1 second, so this whole block is ~20-30s.
    const soakSeeds = [
      1001, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051,
      1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117,
    ];
    for (const seed of soakSeeds) {
      it(`soak seed ${seed} — 2 years, no errors, no invariant violations`, async () => {
        const r = await simulateLifetime({
          seed,
          characterAge: 12,
          maxMinutes: 2 * ONE_YEAR_MIN,
        });
        expect(r.errors, summarize(r)).toEqual([]);
        expect(r.invariantViolations, summarize(r)).toEqual([]);
      }, 30_000);
    }
  });
});
