import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it, beforeEach } from 'vitest';
import { COMMAND_REGISTRY } from '../../src/handlers/index.js';
import { extractPayload } from '../../src/contracts/extract-payload.js';
import { resolvePayloadId } from '../../src/handlers/payloadHelpers.js';
import { WEBSOCKET_COMMANDS } from '../../src/contracts/websocket-commands.js';
import {
  createTestPlayer,
  MockPlayerSession,
  seedSmokingHabit,
  seedStoreItem,
  seedNpc,
  seedExtracurricularId,
} from './helpers.js';
import { getStoreItems } from '../../src/services/shop/shop_manager.js';
import { getExtracurriculars } from '../../src/services/education/index.js';

interface GoldenFixture {
  command: string;
  variant: string;
  envelope: Record<string, unknown>;
  setup: string;
  invokeHandler?: boolean;
}

const fixtures: GoldenFixture[] = JSON.parse(
  fs.readFileSync(path.join(__dirname, 'fixtures', 'commands.json'), 'utf8')
);

const MISSING_ID_PATTERNS = [
  /missing debug/i,
  /missing partnerId/i,
  /invalid payload shape/i,
  /provide money, energy/i,
];

const HANDLER_SUCCESS_TYPES: Record<string, string[]> = {
  quitHabit: ['habitQuitting'],
  stopQuitHabit: ['habitQuitStopped'],
  deviceToken: [],
  retrievePerson: ['personObject'],
  deleteAccount: ['accountDeletionScheduled', 'accountDeleted'],
};

function applySetup(setup: string, session: MockPlayerSession): string | undefined {
  switch (setup) {
    case 'smoking-habit':
      seedSmokingHabit(session.player);
      return 'smoking';
    case 'quitting-habit': {
      seedSmokingHabit(session.player);
      const habit = session.player.c.habits?.[0];
      if (habit) habit.status = 'quitting';
      return 'smoking';
    }
    case 'adult-player':
      session.player.c.ageYears = 22;
      return 'cashier';
    case 'store-item':
      return seedStoreItem(session.player);
    case 'extracurricular-id':
      return seedExtracurricularId();
    case 'default':
      seedNpc(session.player);
      return undefined;
  }
}

function replacePlaceholders(
  value: unknown,
  placeholder?: string
): unknown {
  if (typeof value === 'string' && value === 'PLACEHOLDER_ACTIVITY' && placeholder) {
    return placeholder;
  }
  if (typeof value === 'string' && value === 'PLACEHOLDER_ITEM' && placeholder) {
    return placeholder;
  }
  if (Array.isArray(value)) {
    return value.map((v) => replacePlaceholders(v, placeholder));
  }
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value as Record<string, unknown>).map(([k, v]) => [
        k,
        replacePlaceholders(v, placeholder),
      ])
    );
  }
  return value;
}

describe('payload matrix (golden fixtures)', () => {
  for (const fixture of fixtures) {
    it(`${fixture.command} [${fixture.variant}] resolves payload without missing-id errors`, async () => {
      const handler = COMMAND_REGISTRY[fixture.command];
      expect(handler, `no handler for ${fixture.command}`).toBeTypeOf('function');

      const player = createTestPlayer();
      const session = new MockPlayerSession(player);
      const placeholder = applySetup(fixture.setup, session);

      const envelope = replacePlaceholders(fixture.envelope, placeholder) as Record<
        string,
        unknown
      >;
      const { commandType, payload } = extractPayload(envelope);
      expect(commandType).toBe(fixture.command);

      const contract = WEBSOCKET_COMMANDS[fixture.command];
      if (contract?.idKeys?.length) {
        const resolvedId = resolvePayloadId(payload, ...contract.idKeys);
        expect(resolvedId, `payload ID not resolved for ${fixture.variant}`).toBeDefined();
      }

      if (fixture.invokeHandler === false) {
        return;
      }

      await handler(payload, session as never);

      for (const pattern of MISSING_ID_PATTERNS) {
        expect(
          session.sentErrorMatching(pattern),
          `unexpected missing-id error for ${fixture.command} ${fixture.variant}`
        ).toBe(false);
      }

      const expectedTypes = HANDLER_SUCCESS_TYPES[fixture.command];
      if (expectedTypes && expectedTypes.length > 0) {
        const matched = expectedTypes.some((t) => session.getMessagesByType(t).length > 0);
        expect(matched, `expected success response for ${fixture.command}`).toBe(true);
      }
    });
  }

  describe('ID command coverage', () => {
    const idCommands = Object.entries(WEBSOCKET_COMMANDS).filter(
      ([, c]) => c.idKeys && c.idKeys.length > 0
    );

    it('declares idKeys for all high-risk ID commands', () => {
      expect(idCommands.map(([n]) => n)).toEqual(
        expect.arrayContaining([
          'quitHabit',
          'applyForJob',
          'purchaseItem',
          'retrievePerson',
          'applyForExtracurricular',
        ])
      );
    });
  });
});

describe('payload matrix — handler outcomes', () => {
  let session: MockPlayerSession;

  beforeEach(() => {
    session = new MockPlayerSession(createTestPlayer());
    seedSmokingHabit(session.player);
  });

  it('quitHabit legacy string starts quitting flow', async () => {
    const { payload } = extractPayload({ type: 'quitHabit', message: 'smoking' });
    await COMMAND_REGISTRY.quitHabit(payload, session as never);

    expect(session.getMessagesByType('habitQuitting').length).toBe(1);
    expect(session.sentErrorMatching(/not found/i)).toBe(false);
  });

  it('stopQuitHabit restores active habit from quitting state', async () => {
    const habit = session.player.c.habits?.[0];
    if (habit) habit.status = 'quitting';

    const { payload } = extractPayload({ type: 'stopQuitHabit', message: 'smoking' });
    await COMMAND_REGISTRY.stopQuitHabit(payload, session as never);

    expect(session.getMessagesByType('habitQuitStopped').length).toBe(1);
  });
});
