import { createHash } from 'node:crypto';
import argon2 from 'argon2';
import type pg from 'pg';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { buildApp } from '../build-app.js';
import { createPool } from '../../infrastructure/db/pool.js';
import {
  getTestDbUrl,
  recreateDatabase,
  runMigrations,
} from '../../infrastructure/db/tests/db-test-support.js';

const hasDb = Boolean(process.env.TEST_DATABASE_URL);
const ADMIN_ID = '10000000-0000-4000-8000-000000000187';
const CASHIER_ID = '20000000-0000-4000-8000-000000000187';
const MANAGER_ID = '30000000-0000-4000-8000-000000000187';
const TERMINAL_ID = '40000000-0000-4000-8000-000000000187';
const STORE_ID = '00000000-0000-0000-0000-000000000001';
const CASH_SESSION_ID = '50000000-0000-4000-8000-000000000187';
const ADMIN_TOKEN = 'f187-admin-session-token';
const CASHIER_TOKEN = 'f187-cashier-session-token';
const PASSWORD = 'cashier-password-187';

function tokenHash(token: string): string {
  return createHash('sha256').update(token).digest('hex');
}

function cookie(token: string): string {
  return `backoffice_session=${token}`;
}

describe.skipIf(!hasDb)('F-187 POS cashier lifecycle (real PostgreSQL)', () => {
  const url = hasDb ? getTestDbUrl() : '';
  let pool: pg.Pool;
  let app: Awaited<ReturnType<typeof buildApp>>;

  beforeAll(async () => {
    await recreateDatabase(url);
    await runMigrations(url, 'up');
    pool = createPool(url);
    const passwordHash = await argon2.hash(PASSWORD);
    await pool.query(
      `INSERT INTO backoffice_users (id, email, password_hash, role)
       VALUES
         ($1, 'admin-f187@example.test', $4, 'admin'),
         ($2, 'cashier-f187@example.test', $4, 'pos_cashier'),
         ($3, 'manager-f187@example.test', $4, 'pos_manager')`,
      [ADMIN_ID, CASHIER_ID, MANAGER_ID, passwordHash],
    );
    await pool.query(
      `INSERT INTO backoffice_sessions (user_id, token_hash, expires_at)
       VALUES ($1, $3, now() + interval '1 hour'),
              ($2, $4, now() + interval '1 hour')`,
      [ADMIN_ID, CASHIER_ID, tokenHash(ADMIN_TOKEN), tokenHash(CASHIER_TOKEN)],
    );
    await pool.query(
      `INSERT INTO pos_terminals (id, store_id, name, binding_code, bound_at)
       VALUES ($1, $2, 'Caja F-187', 'F187CODE', now())`,
      [TERMINAL_ID, STORE_ID],
    );
    app = await buildApp({ pool, cookieSecure: false });
  });

  afterAll(async () => {
    await app.close();
    await pool.end();
  });

  it('lists lifecycle status for admins and rejects non-admin listing', async () => {
    const listed = await app.inject({
      method: 'GET',
      url: '/pos/users',
      headers: { cookie: cookie(ADMIN_TOKEN) },
    });
    expect(listed.statusCode).toBe(200);
    expect(listed.json().items).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          id: CASHIER_ID,
          role: 'pos_cashier',
          active: true,
          status: 'active',
          deletedAt: null,
        }),
      ]),
    );

    const forbidden = await app.inject({
      method: 'GET',
      url: '/pos/users',
      headers: { cookie: cookie(CASHIER_TOKEN) },
    });
    expect(forbidden.statusCode).toBe(403);
  });

  it('deactivates, revokes sessions, blocks auth and can reactivate without restoring sessions', async () => {
    const deactivated = await app.inject({
      method: 'PATCH',
      url: `/pos/users/${CASHIER_ID}/status`,
      headers: { cookie: cookie(ADMIN_TOKEN) },
      payload: { active: false },
    });
    expect(deactivated.statusCode).toBe(200);
    expect(deactivated.json()).toMatchObject({ id: CASHIER_ID, active: false, status: 'inactive' });

    const session = await pool.query<{ revoked_at: Date | null }>(
      'SELECT revoked_at FROM backoffice_sessions WHERE user_id = $1',
      [CASHIER_ID],
    );
    expect(session.rows[0]?.revoked_at).toBeInstanceOf(Date);

    const oldSession = await app.inject({
      method: 'GET',
      url: '/pos/config',
      headers: { cookie: cookie(CASHIER_TOKEN), 'x-terminal-id': TERMINAL_ID },
    });
    expect(oldSession.statusCode).toBe(401);

    const blockedLogin = await app.inject({
      method: 'POST',
      url: '/backoffice/auth/login',
      payload: { email: 'cashier-f187@example.test', password: PASSWORD },
    });
    expect(blockedLogin.statusCode).toBe(401);
    expect(blockedLogin.json().error.code).toBe('INVALID_CREDENTIALS');

    const reactivated = await app.inject({
      method: 'PATCH',
      url: `/pos/users/${CASHIER_ID}/status`,
      headers: { cookie: cookie(ADMIN_TOKEN) },
      payload: { active: true },
    });
    expect(reactivated.statusCode).toBe(200);
    expect(reactivated.json()).toMatchObject({ active: true, status: 'active' });

    const revokedStill = await pool.query<{ revoked_at: Date | null }>(
      'SELECT revoked_at FROM backoffice_sessions WHERE user_id = $1',
      [CASHIER_ID],
    );
    expect(revokedStill.rows[0]?.revoked_at).toBeInstanceOf(Date);

    const login = await app.inject({
      method: 'POST',
      url: '/backoffice/auth/login',
      payload: { email: 'cashier-f187@example.test', password: PASSWORD },
    });
    expect(login.statusCode).toBe(200);
  });

  it('blocks lifecycle mutations during an open cash session', async () => {
    await pool.query(
      `INSERT INTO pos_cash_sessions
         (id, terminal_id, store_id, user_id, opening_cash_cents)
       VALUES ($1, $2, $3, $4, 1000)`,
      [CASH_SESSION_ID, TERMINAL_ID, STORE_ID, CASHIER_ID],
    );

    for (const request of [
      { method: 'PATCH' as const, payload: { active: false } },
      { method: 'DELETE' as const, payload: undefined },
    ]) {
      const response = await app.inject({
        method: request.method,
        url:
          request.method === 'PATCH'
            ? `/pos/users/${CASHIER_ID}/status`
            : `/pos/users/${CASHIER_ID}`,
        headers: { cookie: cookie(ADMIN_TOKEN) },
        ...(request.payload ? { payload: request.payload } : {}),
      });
      expect(response.statusCode).toBe(409);
      expect(response.json().error.code).toBe('POS_CASHIER_HAS_OPEN_SESSION');
    }
  });

  it('soft-deletes after close, preserves attribution and rejects reactivation', async () => {
    await pool.query(
      `UPDATE pos_cash_sessions SET status = 'CLOSED', closed_at = now(), updated_at = now()
       WHERE id = $1`,
      [CASH_SESSION_ID],
    );

    const deleted = await app.inject({
      method: 'DELETE',
      url: `/pos/users/${CASHIER_ID}`,
      headers: { cookie: cookie(ADMIN_TOKEN) },
    });
    expect(deleted.statusCode).toBe(204);

    const preserved = await pool.query<{
      id: string;
      active: boolean;
      deleted_at: Date | null;
      historical_user_id: string;
    }>(
      `SELECT u.id, u.active, u.deleted_at, cs.user_id AS historical_user_id
         FROM backoffice_users u
         JOIN pos_cash_sessions cs ON cs.user_id = u.id
        WHERE u.id = $1 AND cs.id = $2`,
      [CASHIER_ID, CASH_SESSION_ID],
    );
    expect(preserved.rows[0]).toMatchObject({
      id: CASHIER_ID,
      active: false,
      historical_user_id: CASHIER_ID,
    });
    expect(preserved.rows[0]?.deleted_at).toBeInstanceOf(Date);

    const reactivate = await app.inject({
      method: 'PATCH',
      url: `/pos/users/${CASHIER_ID}/status`,
      headers: { cookie: cookie(ADMIN_TOKEN) },
      payload: { active: true },
    });
    expect(reactivate.statusCode).toBe(409);
    expect(reactivate.json().error.code).toBe('POS_CASHIER_DELETED');

    const managerTarget = await app.inject({
      method: 'PATCH',
      url: `/pos/users/${MANAGER_ID}/status`,
      headers: { cookie: cookie(ADMIN_TOKEN) },
      payload: { active: false },
    });
    expect(managerTarget.statusCode).toBe(404);
    expect(managerTarget.json().error.code).toBe('POS_CASHIER_NOT_FOUND');

    const audit = await pool.query<{ action: string }>(
      `SELECT action FROM security_audit_log WHERE target = $1 ORDER BY created_at`,
      [CASHIER_ID],
    );
    expect(audit.rows.map((row) => row.action)).toEqual([
      'pos.cashier.deactivated',
      'pos.cashier.reactivated',
      'pos.cashier.deleted',
    ]);
  });
});
