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 });
}

function cookieValue(setCookieHeader: string | string[] | undefined): string {
  const raw = Array.isArray(setCookieHeader) ? setCookieHeader[0] : setCookieHeader;
  expect(raw).toBeDefined();
  const pair = (raw as string).split(';')[0] as string;
  return pair.slice(pair.indexOf('=') + 1);
}

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

  const ana = { email: 'ana@example.com', password: 'correct horse battery staple' };
  const ben = { email: 'ben@example.com', password: 'another valid password' };
  let anaId = '';
  let anaCookie = '';
  let benId = '';
  let benCookie = '';

  async function registerAndLogin(user: { email: string; password: string }) {
    const registered = await app.inject({
      method: 'POST',
      url: '/auth/register',
      headers: { 'content-type': 'application/json' },
      payload: user,
    });
    expect(registered.statusCode).toBe(201);
    const login = await app.inject({
      method: 'POST',
      url: '/auth/login',
      headers: { 'content-type': 'application/json' },
      payload: user,
    });
    expect(login.statusCode).toBe(200);
    const body = login.json() as { id: string; role: string };
    expect(body.role).toBe('customer');
    return { id: body.id, cookie: cookieValue(login.headers['set-cookie']) };
  }

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

    const a = await registerAndLogin(ana);
    anaId = a.id;
    anaCookie = a.cookie;
    const b = await registerAndLogin(ben);
    benId = b.id;
    benCookie = b.cookie;
  });

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

  it('unauthenticated and bad-cookie requests get 401', async () => {
    const missing = await app.inject({ method: 'GET', url: `/users/${anaId}` });
    expect(missing.statusCode).toBe(401);
    expect(missing.json().error.code).toBe('UNAUTHORIZED');

    const forged = await app.inject({
      method: 'GET',
      url: `/users/${anaId}`,
      cookies: { [SESSION_COOKIE_NAME]: 'forged-token' },
    });
    expect(forged.statusCode).toBe(401);
  });

  it('user A requesting user B profile gets 403, own flow works (AC1)', async () => {
    const cross = await app.inject({
      method: 'GET',
      url: `/users/${benId}`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(cross.statusCode).toBe(403);
    expect(cross.json().error.code).toBe('FORBIDDEN');

    // Customer detail exists even before optional profile fields are populated.
    const ownCustomer = await app.inject({
      method: 'GET',
      url: `/users/${anaId}`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(ownCustomer.statusCode).toBe(200);
    expect(ownCustomer.json()).toMatchObject({ id: anaId, email: 'ana@example.com' });
  });

  it('PATCH own profile upserts; PATCH is validated', async () => {
    const emptyBody = await app.inject({
      method: 'PATCH',
      url: `/users/${anaId}`,
      headers: { 'content-type': 'application/json' },
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
      payload: {},
    });
    expect(emptyBody.statusCode).toBe(400);

    const created = await app.inject({
      method: 'PATCH',
      url: `/users/${anaId}`,
      headers: { 'content-type': 'application/json' },
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
      payload: { displayName: 'Ana', phone: '+54 11 5555 0001' },
    });
    expect(created.statusCode).toBe(200);
    expect(created.json().displayName).toBe('Ana');

    const partial = await app.inject({
      method: 'PATCH',
      url: `/users/${anaId}`,
      headers: { 'content-type': 'application/json' },
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
      payload: { phone: '+54 11 5555 0002' },
    });
    expect(partial.statusCode).toBe(200);
    const body = partial.json();
    expect(body.phone).toBe('+54 11 5555 0002');
    expect(body.displayName).toBe('Ana'); // untouched field preserved
  });

  it('GET /users is admin-only: customer 403, admin 200 (AC2, AC3)', async () => {
    const asCustomer = await app.inject({
      method: 'GET',
      url: '/users',
      cookies: { [SESSION_COOKIE_NAME]: benCookie },
    });
    expect(asCustomer.statusCode).toBe(403);

    // Promotion happens out-of-band (DB); session cookie picks up the new role.
    await pool.query('UPDATE identity_users SET role = $1 WHERE id = $2', ['admin', anaId]);

    const asAdmin = await app.inject({
      method: 'GET',
      url: '/users',
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(asAdmin.statusCode).toBe(200);
    const body = asAdmin.json() as { items: Array<{ id: string }> };
    // F-154: /users now serves storefront customers only (role 'customer').
    // ana was promoted to 'admin' -> no longer a customer -> excluded from the list.
    // ben is a storefront customer -> included.
    expect(body.items.some((item) => item.id === benId)).toBe(true);
    expect(body.items.some((item) => item.id === anaId)).toBe(false);

    // Admin can also read another user's profile (owner-or-admin).
    const adminReadsBen = await app.inject({
      method: 'GET',
      url: `/users/${benId}`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    // Customer detail remains available when optional profile fields are absent.
    expect(adminReadsBen.statusCode).toBe(200);
    expect(adminReadsBen.json()).toMatchObject({ id: benId, email: 'ben@example.com' });
  });

  it('address CRUD works end to end for own addresses (AC4)', async () => {
    const addressInput = {
      label: 'Casa',
      recipientName: 'Ana Torres',
      street: 'Av. Siempre Viva 742',
      city: 'Springfield',
      postalCode: '1000',
      country: 'AR',
      isDefault: true,
    };

    const created = await app.inject({
      method: 'POST',
      url: `/users/${anaId}/addresses`,
      headers: { 'content-type': 'application/json' },
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
      payload: addressInput,
    });
    expect(created.statusCode).toBe(201);
    const address = created.json();
    expect(address.id).toMatch(/^[0-9a-f-]{36}$/);
    expect(address.isDefault).toBe(true);

    const invalid = await app.inject({
      method: 'POST',
      url: `/users/${anaId}/addresses`,
      headers: { 'content-type': 'application/json' },
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
      payload: { label: 'sin destinatario' },
    });
    expect(invalid.statusCode).toBe(400);

    const list = await app.inject({
      method: 'GET',
      url: `/users/${anaId}/addresses`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(list.statusCode).toBe(200);
    expect(list.json().items).toHaveLength(1);

    const updated = await app.inject({
      method: 'PATCH',
      url: `/users/${anaId}/addresses/${address.id}`,
      headers: { 'content-type': 'application/json' },
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
      payload: { city: 'Shelbyville', isDefault: false },
    });
    expect(updated.statusCode).toBe(200);
    expect(updated.json().city).toBe('Shelbyville');
    expect(updated.json().isDefault).toBe(false);

    const deleted = await app.inject({
      method: 'DELETE',
      url: `/users/${anaId}/addresses/${address.id}`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(deleted.statusCode).toBe(204);

    const empty = await app.inject({
      method: 'GET',
      url: `/users/${anaId}/addresses`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(empty.json().items).toHaveLength(0);
  });

  it("users cannot mutate addresses on another user's resource -> 403", async () => {
    const created = await app.inject({
      method: 'POST',
      url: `/users/${benId}/addresses`,
      headers: { 'content-type': 'application/json' },
      cookies: { [SESSION_COOKIE_NAME]: benCookie },
      payload: {
        recipientName: 'Ben',
        street: 'Calle 1',
        city: 'CABA',
        postalCode: '1400',
        country: 'AR',
      },
    });
    expect(created.statusCode).toBe(201);
    const addressId = created.json().id as string;

    // Fresh regular customer (Ana was promoted to admin earlier, so she no
    // longer represents a plain outsider).
    const outsider = await registerAndLogin({
      email: 'dani@example.com',
      password: 'dani valid password',
    });

    for (const attempt of [
      {
        method: 'POST',
        url: `/users/${benId}/addresses`,
        payload: {
          recipientName: 'Intruso',
          street: 'x',
          city: 'x',
          postalCode: 'x',
          country: 'x',
        },
      },
      { method: 'PATCH', url: `/users/${benId}/addresses/${addressId}`, payload: { city: 'x' } },
      { method: 'DELETE', url: `/users/${benId}/addresses/${addressId}`, payload: undefined },
    ]) {
      const response = await app.inject({
        method: attempt.method as 'POST' | 'PATCH' | 'DELETE',
        url: attempt.url,
        headers: attempt.payload === undefined ? {} : { 'content-type': 'application/json' },
        cookies: { [SESSION_COOKIE_NAME]: outsider.cookie },
        payload: attempt.payload,
      });
      expect(response.statusCode).toBe(403);
    }

    // Ana is admin now (previous test promoted her): admin bypasses ownership.
    const adminDelete = await app.inject({
      method: 'DELETE',
      url: `/users/${benId}/addresses/${addressId}`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(adminDelete.statusCode).toBe(204);
  });

  it('unknown address on own resource -> 404; malformed uuid -> 400', async () => {
    const ghostId = '00000000-0000-0000-0000-000000000000';
    const missing = await app.inject({
      method: 'DELETE',
      url: `/users/${anaId}/addresses/${ghostId}`,
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(missing.statusCode).toBe(404);

    const malformed = await app.inject({
      method: 'GET',
      url: '/users/not-a-uuid',
      cookies: { [SESSION_COOKIE_NAME]: anaCookie },
    });
    expect(malformed.statusCode).toBe(400);
    expect(malformed.json().error.code).toBe('VALIDATION_ERROR');
  });

  it('logout kills access to protected routes (F-005 regression)', async () => {
    const session = await registerAndLogin({
      email: 'carla@example.com',
      password: 'carla pass 123',
    });
    const logout = await app.inject({
      method: 'POST',
      url: '/auth/logout',
      cookies: { [SESSION_COOKIE_NAME]: session.cookie },
    });
    expect(logout.statusCode).toBe(204);

    const after = await app.inject({
      method: 'GET',
      url: `/users/${session.id}`,
      cookies: { [SESSION_COOKIE_NAME]: session.cookie },
    });
    expect(after.statusCode).toBe(401);
  });
});
