import type { DestinationStream } from 'pino';
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 { createLogger } from '../../infrastructure/logging/logger.js';
import {
  getTestDbUrl,
  recreateDatabase,
  runMigrations,
} from '../../infrastructure/db/tests/db-test-support.js';
import { SESSION_COOKIE_NAME } from '../../modules/identity/index.js';

const hasDb = Boolean(process.env.TEST_DATABASE_URL);

function silentLogger() {
  const destination: DestinationStream = { write: () => undefined };
  return createLogger({ level: 'info', destination });
}

interface ErrorBody {
  error: { statusCode: number; code: string; message: string };
  requestId: string;
}

function parseSetCookie(header: string | string[] | undefined): {
  value: string;
  attrs: string;
} {
  const raw = Array.isArray(header) ? header[0] : header;
  expect(raw).toBeDefined();
  const [pair, ...rest] = (raw as string).split(';');
  const eq = (pair as string).indexOf('=');
  return { value: (pair as string).slice(eq + 1), attrs: rest.join(';').toLowerCase() };
}

describe.skipIf(!hasDb)('identity flows (real PostgreSQL)', () => {
  const url = hasDb ? getTestDbUrl() : '';
  let pool: pg.Pool;
  let app: Awaited<ReturnType<typeof buildApp>>;

  const credentials = {
    email: 'ana@example.com',
    password: 'correct horse battery staple',
  };

  beforeAll(async () => {
    await recreateDatabase(url);
    await runMigrations(url, 'up');
    pool = createPool(url);
    app = await buildApp({ logger: silentLogger(), pool, cookieSecure: true });
  });

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

  it('register stores an argon2 hash, never the plaintext (AC3)', async () => {
    const response = await app.inject({
      method: 'POST',
      url: '/auth/register',
      headers: { 'content-type': 'application/json' },
      payload: credentials,
    });

    expect(response.statusCode).toBe(201);
    const body = response.json() as { id: string; email: string; createdAt: string };
    expect(body.id).toMatch(/^[0-9a-f-]{36}$/);
    expect(body.email).toBe(credentials.email);

    const row = await pool.query('SELECT email, password_hash FROM identity_users');
    expect(row.rowCount).toBe(1);
    const stored = row.rows[0] as { email: string; password_hash: string };
    expect(stored.password_hash.startsWith('$argon2id$')).toBe(true);
    expect(stored.password_hash).not.toContain(credentials.password);
    expect(JSON.stringify(stored)).not.toContain(credentials.password);
  });

  it('register duplicate email -> 409', async () => {
    const response = await app.inject({
      method: 'POST',
      url: '/auth/register',
      headers: { 'content-type': 'application/json' },
      payload: { ...credentials, password: 'another-valid-password' },
    });
    expect(response.statusCode).toBe(409);
    const body = response.json() as ErrorBody;
    expect(body.error.code).toBe('EMAIL_ALREADY_REGISTERED');
  });

  it('register invalid payload -> 400 VALIDATION_ERROR', async () => {
    const response = await app.inject({
      method: 'POST',
      url: '/auth/register',
      headers: { 'content-type': 'application/json' },
      payload: { email: 'not-an-email', password: 'short' },
    });
    expect(response.statusCode).toBe(400);
    const body = response.json() as ErrorBody;
    expect(body.error.code).toBe('VALIDATION_ERROR');
  });

  it('login with valid credentials -> 200 + secure HttpOnly SameSite cookie (AC1, AC5)', async () => {
    const response = await app.inject({
      method: 'POST',
      url: '/auth/login',
      headers: { 'content-type': 'application/json' },
      payload: credentials,
    });

    expect(response.statusCode).toBe(200);
    const body = response.json() as { id: string; email: string };
    expect(body.email).toBe(credentials.email);

    const cookieHeader = response.headers['set-cookie'];
    const raw = Array.isArray(cookieHeader) ? cookieHeader[0] : cookieHeader;
    expect(typeof raw).toBe('string');
    expect(raw).toContain(`${SESSION_COOKIE_NAME}=`);
    expect(raw?.toLowerCase()).toContain('httponly');
    expect(raw?.toLowerCase()).toContain('secure');
    expect(raw?.toLowerCase()).toContain('samesite=lax');
    expect(raw?.toLowerCase()).toContain('max-age=');

    const sessionRow = await pool.query(
      'SELECT token_hash, expires_at, revoked_at FROM identity_sessions',
    );
    expect(sessionRow.rowCount).toBe(1);
    const session = sessionRow.rows[0] as { token_hash: string; revoked_at: Date | null };
    expect(session.token_hash).toMatch(/^[0-9a-f]{64}$/);
    expect(session.revoked_at).toBeNull();
  });

  it('wrong password and unknown email return identical 401 (AC2, no enumeration)', async () => {
    const wrongPassword = await app.inject({
      method: 'POST',
      url: '/auth/login',
      headers: { 'content-type': 'application/json' },
      payload: { email: credentials.email, password: 'definitely-wrong-password' },
    });
    const unknownEmail = await app.inject({
      method: 'POST',
      url: '/auth/login',
      headers: { 'content-type': 'application/json' },
      payload: { email: 'ghost@example.com', password: 'definitely-wrong-password' },
    });

    expect(wrongPassword.statusCode).toBe(401);
    expect(unknownEmail.statusCode).toBe(401);

    const bodyA = wrongPassword.json() as ErrorBody;
    const bodyB = unknownEmail.json() as ErrorBody;
    expect(bodyA.error).toEqual(bodyB.error);
    expect(bodyA.error.code).toBe('INVALID_CREDENTIALS');
    expect(bodyA.error.message).toBe('Invalid credentials');
  });

  it('logout revokes the session and is idempotent', async () => {
    const loginResponse = await app.inject({
      method: 'POST',
      url: '/auth/login',
      headers: { 'content-type': 'application/json' },
      payload: credentials,
    });
    expect(loginResponse.statusCode).toBe(200);
    const cookie = parseSetCookie(loginResponse.headers['set-cookie']);

    const logoutResponse = await app.inject({
      method: 'POST',
      url: '/auth/logout',
      cookies: { [SESSION_COOKIE_NAME]: cookie.value },
    });
    expect(logoutResponse.statusCode).toBe(204);
    const cleared = logoutResponse.headers['set-cookie'];
    const clearedRaw = Array.isArray(cleared) ? cleared[0] : cleared;
    expect(clearedRaw?.toLowerCase()).toContain('expires=');

    const revoked = await pool.query(
      'SELECT count(*)::int AS n FROM identity_sessions WHERE revoked_at IS NOT NULL',
    );
    expect(revoked.rows[0]?.n).toBeGreaterThanOrEqual(1);

    const secondLogout = await app.inject({
      method: 'POST',
      url: '/auth/logout',
      cookies: { [SESSION_COOKIE_NAME]: cookie.value },
    });
    expect(secondLogout.statusCode).toBe(204);
  });

  it('10 failed logins in a row -> next attempt gets 429 with Retry-After (AC4)', async () => {
    const target = { email: 'locked@example.com', password: 'some-valid-password-1' };
    const registered = await app.inject({
      method: 'POST',
      url: '/auth/register',
      headers: { 'content-type': 'application/json' },
      payload: target,
    });
    expect(registered.statusCode).toBe(201);

    for (let i = 0; i < 10; i += 1) {
      const attempt = await app.inject({
        method: 'POST',
        url: '/auth/login',
        headers: { 'content-type': 'application/json' },
        payload: { email: target.email, password: 'wrong-password-attempt' },
      });
      expect(attempt.statusCode).toBe(401);
    }

    const blocked = await app.inject({
      method: 'POST',
      url: '/auth/login',
      headers: { 'content-type': 'application/json' },
      payload: target,
    });
    expect(blocked.statusCode).toBe(429);
    const body = blocked.json() as ErrorBody;
    expect(body.error.code).toBe('TOO_MANY_ATTEMPTS');
    const retryAfter = Number(blocked.headers['retry-after']);
    expect(retryAfter).toBeGreaterThan(0);
  });

  it('foundation-only app (no pool) keeps /auth routes unregistered', async () => {
    const bare = await buildApp({ logger: silentLogger() });
    const response = await bare.inject({
      method: 'POST',
      url: '/auth/login',
      headers: { 'content-type': 'application/json' },
      payload: credentials,
    });
    expect(response.statusCode).toBe(404);
    await bare.close();
  });
});
