import { describe, it, expect, beforeAll, vi } from 'vitest';
import { execFileSync } from 'child_process';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

// Env must be pinned before the db singleton is imported (dotenv never overrides).
const dir = mkdtempSync(join(tmpdir(), 'door-test-'));
const dbPath = join(dir, 'test.db');
process.env.DB_TYPE = 'sqlite';
process.env.DATABASE_URL = dbPath;
process.env.FRONTEND_URL = 'http://localhost:3002';
process.env.BETTER_AUTH_SECRET = 'door-test-secret-0123456789abcdef';
delete process.env.REDIS_URL;

const STAFF = { id: 'staff-user-id', name: 'Door Staff', role: 'staff' };

// The door endpoints are behind staff auth; the flows under test are the writes,
// not Better Auth, which has its own integration suite.
vi.mock('../lib/auth.js', () => ({
  requireAuth: () => async (c: any, next: any) => {
    c.set('user', STAFF);
    await next();
  },
  getAuthUser: async () => STAFF,
}));

// Walk-ins with an email trigger a confirmation send; keep it out of the test.
vi.mock('../lib/email.js', () => ({
  default: { sendBookingConfirmation: vi.fn(async () => ({ success: true })) },
}));

let app: any;
let sqlite: any;

const EVENT_ID = 'evt-door-1';
const PRICE = 60000;

/** POST helper that mirrors how the door screen calls the API. */
async function post(path: string, body: unknown) {
  const res = await app.request(path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

async function get(path: string) {
  const res = await app.request(path);
  return { status: res.status, body: await res.json() };
}

function seedTicket(row: {
  id: string;
  first: string;
  last?: string | null;
  status: string;
  paymentStatus: string;
  phone?: string | null;
  bookingId?: string | null;
  qr?: string;
}) {
  sqlite
    .prepare(
      `INSERT INTO tickets (id, booking_id, user_id, event_id, attendee_first_name, attendee_last_name,
        attendee_email, attendee_phone, status, payment_status, is_guest, qr_code, created_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`
    )
    .run(
      row.id,
      row.bookingId ?? null,
      'seed-user',
      EVENT_ID,
      row.first,
      row.last ?? null,
      `${row.id}@test.py`,
      row.phone ?? null,
      row.status,
      row.paymentStatus,
      row.qr ?? `QR-${row.id}`,
      new Date().toISOString()
    );
}

beforeAll(() => {
  execFileSync('npx', ['tsx', 'src/db/migrate.ts'], { env: { ...process.env }, stdio: 'pipe' });

  return (async () => {
    const { Hono } = await import('hono');
    const doorRoutes = (await import('./door.js')).default;
    app = new Hono();
    app.route('/api/events', doorRoutes);

    const Database = (await import('better-sqlite3')).default;
    sqlite = new Database(dbPath);

    const now = new Date().toISOString();
    sqlite
      .prepare(
        `INSERT INTO users (id, email, name, role, is_claimed, account_status, created_at, updated_at)
         VALUES (?, ?, ?, 'user', 0, 'unclaimed', ?, ?)`
      )
      .run('seed-user', 'seed@test.py', 'Seed User', now, now);
    sqlite
      .prepare(
        `INSERT INTO users (id, email, name, role, is_claimed, account_status, created_at, updated_at)
         VALUES (?, ?, ?, 'staff', 1, 'active', ?, ?)`
      )
      .run(STAFF.id, 'staff@test.py', STAFF.name, now, now);
    sqlite
      .prepare(
        `INSERT INTO events (id, title, description, start_datetime, location, price, currency, capacity, status, created_at, updated_at)
         VALUES (?, 'Door Night', 'desc', ?, 'Asuncion', ?, 'PYG', 2, 'published', ?, ?)`
      )
      .run(EVENT_ID, now, PRICE, now, now);

    seedTicket({ id: 'tkt-paid', first: 'José', last: 'Núñez', status: 'confirmed', paymentStatus: 'paid', phone: '+595 981 234 567' });
    seedTicket({ id: 'tkt-unpaid', first: 'Ana', last: 'Group', status: 'confirmed', paymentStatus: 'unpaid', bookingId: 'bk-1' });
    seedTicket({ id: 'tkt-unpaid-2', first: 'Beto', last: 'Group', status: 'confirmed', paymentStatus: 'unpaid', bookingId: 'bk-1' });
    seedTicket({ id: 'tkt-cancelled', first: 'Carla', last: 'Gone', status: 'cancelled', paymentStatus: 'unpaid' });
  })();
}, 120_000);

describe('door-attendees', () => {
  it('returns everyone including cancelled, with group bookings flagged', async () => {
    const { status, body } = await get(`/api/events/${EVENT_ID}/door-attendees`);
    expect(status).toBe(200);
    expect(body.event.price).toBe(PRICE);
    expect(body.attendees).toHaveLength(4);

    const cancelled = body.attendees.find((a: any) => a.ticketId === 'tkt-cancelled');
    expect(cancelled.status).toBe('cancelled');

    const grouped = body.attendees.find((a: any) => a.ticketId === 'tkt-unpaid');
    expect(grouped.isGroupBooking).toBe(true);
    expect(grouped.amountDue).toBe(PRICE);

    const solo = body.attendees.find((a: any) => a.ticketId === 'tkt-paid');
    expect(solo.isGroupBooking).toBe(false);
    expect(solo.amountDue).toBe(0);
  });

  it('is sorted alphabetically so an empty search is scrollable', async () => {
    const { body } = await get(`/api/events/${EVENT_ID}/door-attendees`);
    const names = body.attendees.map((a: any) => a.fullName);
    expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })));
  });
});

describe('door-checkin: existing ticket', () => {
  it('checks in a paid attendee with no payment record touched', async () => {
    const { status, body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      ticketId: 'tkt-paid',
      entryMethod: 'search',
      idempotencyKey: 'key-paid-checkin',
    });
    expect(status).toBe(201);
    expect(body.attendee.checkedIn).toBe(true);
    expect(body.attendee.checkinAt).toBeTruthy();
    expect(body.attendee.checkedInBy).toBe(STAFF.name);
    expect(body.payment).toBeNull();

    const row = sqlite.prepare('SELECT status, checked_in_by_admin_id FROM tickets WHERE id = ?').get('tkt-paid');
    expect(row.status).toBe('checked_in');
    expect(row.checked_in_by_admin_id).toBe(STAFF.id);
  });

  it('replays an already-processed key instead of checking in twice', async () => {
    const before = sqlite.prepare('SELECT checkin_at FROM tickets WHERE id = ?').get('tkt-paid').checkin_at;

    const { status, body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      ticketId: 'tkt-paid',
      idempotencyKey: 'key-paid-checkin',
    });
    expect(status).toBe(200);
    expect(body.replayed).toBe(true);

    const after = sqlite.prepare('SELECT checkin_at FROM tickets WHERE id = ?').get('tkt-paid').checkin_at;
    expect(after).toBe(before);
    expect(sqlite.prepare('SELECT COUNT(*) n FROM payments WHERE ticket_id = ?').get('tkt-paid').n).toBe(0);
  });

  it('settles an unpaid group-booking ticket in cash and checks in, in one call', async () => {
    const { status, body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      ticketId: 'tkt-unpaid',
      payment: { method: 'cash', amount: PRICE },
      entryMethod: 'search',
      idempotencyKey: 'key-unpaid-cash',
    });
    expect(status).toBe(201);
    expect(body.attendee.paymentStatus).toBe('paid');
    expect(body.attendee.checkedIn).toBe(true);
    expect(body.payment).toMatchObject({ method: 'cash', amount: PRICE });

    const payment = sqlite.prepare('SELECT * FROM payments WHERE ticket_id = ?').get('tkt-unpaid');
    expect(payment.source).toBe('door');
    expect(payment.method).toBe('cash');
    expect(payment.provider).toBe('cash');
    expect(payment.status).toBe('paid');
    expect(payment.paid_by_admin_id).toBe(STAFF.id);
  });

  it('takes a group payment at a multiple of the ticket price', async () => {
    const { body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      ticketId: 'tkt-unpaid-2',
      payment: { method: 'transfer', amount: PRICE * 2 },
      idempotencyKey: 'key-unpaid-2-transfer',
    });
    expect(body.payment.amount).toBe(PRICE * 2);
    const payment = sqlite.prepare('SELECT * FROM payments WHERE ticket_id = ?').get('tkt-unpaid-2');
    expect(payment.provider).toBe('bank_transfer');
    expect(payment.method).toBe('transfer');
    expect(payment.amount).toBe(PRICE * 2);
  });

  it('reactivates a cancelled ticket through the same payment flow', async () => {
    const { body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      ticketId: 'tkt-cancelled',
      payment: { method: 'bitcoin' },
      idempotencyKey: 'key-cancelled-reactivate',
    });
    expect(body.attendee.status).toBe('checked_in');
    expect(body.attendee.paymentStatus).toBe('paid');
    const payment = sqlite.prepare('SELECT * FROM payments WHERE ticket_id = ?').get('tkt-cancelled');
    // Bitcoin is recorded as already-paid Lightning: same trust model as cash,
    // no invoice generated (see lib/doorPayments.ts).
    expect(payment.provider).toBe('lightning');
    expect(payment.method).toBe('bitcoin');
    expect(payment.amount).toBe(PRICE);
  });

  it('rejects a ticket from another event', async () => {
    const { status, body } = await post('/api/events/other-event/door-checkin', {
      ticketId: 'tkt-paid',
      idempotencyKey: 'key-wrong-event',
    });
    expect(status).toBe(404);
    expect(body.error).toMatch(/Event not found/);
  });
});

describe('door-checkin: walk-ins', () => {
  it('creates a cash walk-in confirmed, paid and checked in with no email', async () => {
    const { status, body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      attendee: { firstName: 'Walk' },
      payment: { method: 'cash' },
      entryMethod: 'walkin',
      idempotencyKey: 'key-walkin-cash',
    });
    expect(status).toBe(201);
    expect(body.action).toBe('walkin');
    expect(body.attendee.fullName).toBe('Walk');
    expect(body.attendee.checkedIn).toBe(true);
    expect(body.attendee.paymentStatus).toBe('paid');
    expect(body.attendee.email).toBeNull();

    const ticket = sqlite.prepare('SELECT * FROM tickets WHERE id = ?').get(body.attendee.ticketId);
    expect(ticket.status).toBe('checked_in');
    expect(ticket.qr_code).toBeTruthy();
    // A placeholder account keeps users.email unique without mailing anyone.
    const account = sqlite.prepare('SELECT email FROM users WHERE id = ?').get(ticket.user_id);
    expect(account.email).toMatch(/@doorentry\.local$/);
  });

  it('records a guest walk-in as a zero-amount comp', async () => {
    const { body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      attendee: { firstName: 'Free', lastName: 'Guest' },
      payment: { method: 'guest', amount: PRICE },
      entryMethod: 'walkin',
      idempotencyKey: 'key-walkin-guest',
    });
    expect(body.attendee.paymentStatus).toBe('comp');
    expect(body.attendee.isGuest).toBe(true);
    expect(body.payment.amount).toBe(0);
    const payment = sqlite.prepare('SELECT * FROM payments WHERE ticket_id = ?').get(body.attendee.ticketId);
    expect(payment.amount).toBe(0);
    expect(payment.method).toBe('guest');
  });

  it('does not create a second ticket when the same walk-in key is retried', async () => {
    const before = sqlite.prepare('SELECT COUNT(*) n FROM tickets').get().n;
    const { status, body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      attendee: { firstName: 'Walk' },
      payment: { method: 'cash' },
      idempotencyKey: 'key-walkin-cash',
    });
    expect(status).toBe(200);
    expect(body.replayed).toBe(true);
    expect(sqlite.prepare('SELECT COUNT(*) n FROM tickets').get().n).toBe(before);
  });

  it('warns rather than blocks once the event is over capacity', async () => {
    // Capacity is 2 and several tickets already hold seats.
    const { body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      attendee: { firstName: 'Overflow' },
      payment: { method: 'cash' },
      idempotencyKey: 'key-walkin-overflow',
    });
    expect(body.ok).toBe(true);
    expect(body.warnings).toContain('at_capacity');
  });
});

describe('undo', () => {
  it('reverts a plain check-in to its previous state', async () => {
    seedTicket({ id: 'tkt-undo', first: 'Undo', last: 'Me', status: 'confirmed', paymentStatus: 'paid' });
    await post(`/api/events/${EVENT_ID}/door-checkin`, {
      ticketId: 'tkt-undo',
      idempotencyKey: 'key-undo-checkin',
    });
    expect(sqlite.prepare('SELECT status FROM tickets WHERE id = ?').get('tkt-undo').status).toBe('checked_in');

    const { status, body } = await post(`/api/events/${EVENT_ID}/door-checkin/undo`, {
      idempotencyKey: 'key-undo-checkin',
    });
    expect(status).toBe(200);
    expect(body.reverted).toBe('existing');

    const row = sqlite.prepare('SELECT status, checkin_at FROM tickets WHERE id = ?').get('tkt-undo');
    expect(row.status).toBe('confirmed');
    expect(row.checkin_at).toBeNull();
  });

  it('removes the payment it created and restores the unpaid balance', async () => {
    seedTicket({ id: 'tkt-undo-pay', first: 'Undo', last: 'Pay', status: 'confirmed', paymentStatus: 'unpaid' });
    await post(`/api/events/${EVENT_ID}/door-checkin`, {
      ticketId: 'tkt-undo-pay',
      payment: { method: 'cash' },
      idempotencyKey: 'key-undo-pay',
    });
    expect(sqlite.prepare('SELECT COUNT(*) n FROM payments WHERE ticket_id = ?').get('tkt-undo-pay').n).toBe(1);

    await post(`/api/events/${EVENT_ID}/door-checkin/undo`, { idempotencyKey: 'key-undo-pay' });

    const row = sqlite.prepare('SELECT status, payment_status FROM tickets WHERE id = ?').get('tkt-undo-pay');
    expect(row.status).toBe('confirmed');
    expect(row.payment_status).toBe('unpaid');
    expect(sqlite.prepare('SELECT COUNT(*) n FROM payments WHERE ticket_id = ?').get('tkt-undo-pay').n).toBe(0);
  });

  it('cancels a walk-in it created', async () => {
    const { body } = await post(`/api/events/${EVENT_ID}/door-checkin`, {
      attendee: { firstName: 'Mistake' },
      payment: { method: 'cash' },
      idempotencyKey: 'key-undo-walkin',
    });
    await post(`/api/events/${EVENT_ID}/door-checkin/undo`, { idempotencyKey: 'key-undo-walkin' });

    const ticket = sqlite.prepare('SELECT status FROM tickets WHERE id = ?').get(body.attendee.ticketId);
    expect(ticket.status).toBe('cancelled');
    const payment = sqlite.prepare('SELECT status FROM payments WHERE ticket_id = ?').get(body.attendee.ticketId);
    expect(payment.status).toBe('cancelled');
  });

  it('is safe to call twice and rejects an unknown key', async () => {
    const repeat = await post(`/api/events/${EVENT_ID}/door-checkin/undo`, { idempotencyKey: 'key-undo-walkin' });
    expect(repeat.body.alreadyUndone).toBe(true);

    const unknown = await post(`/api/events/${EVENT_ID}/door-checkin/undo`, { idempotencyKey: 'never-happened' });
    expect(unknown.status).toBe(404);
  });
});

describe('door-summary', () => {
  it('totals door takings by tender and splits them from pre-sale', async () => {
    const { status, body } = await get(`/api/events/${EVENT_ID}/door-summary`);
    expect(status).toBe(200);

    // Cash: tkt-unpaid + the 'Walk' and 'Overflow' walk-ins (the undone ones are
    // cancelled and no longer count).
    expect(body.door.byMethod.cash.count).toBe(3);
    expect(body.door.byMethod.cash.total).toBe(PRICE * 3);
    expect(body.door.byMethod.transfer).toEqual({ count: 1, total: PRICE * 2 });
    expect(body.door.byMethod.bitcoin).toEqual({ count: 1, total: PRICE });
    expect(body.door.byMethod.guest).toEqual({ count: 1, total: 0 });
    expect(body.door.total).toBe(PRICE * 6);

    // Settled tickets with no door payment against them: tkt-paid, plus tkt-undo,
    // whose door check-in was undone and which is a pre-paid ticket again.
    expect(body.presale.count).toBe(2);
    expect(body.presale.total).toBe(PRICE * 2);
    expect(body.total).toBe(PRICE * 8);

    expect(body.door.lines.length).toBe(body.door.count);
    expect(body.door.lines[0]).toHaveProperty('name');
  });
});
