import { afterEach, describe, expect, it, vi } from 'vitest';
import type pg from 'pg';
import Fastify, { type FastifyInstance } from 'fastify';
import { AppError, errorEnvelope } from '../../../shared/errors.js';
import { registerReportingRoutes } from './reporting.routes.js';
import type { ReportingRoutesDeps } from '../index.js';

const created: FastifyInstance[] = [];

const ADMIN = { id: 'a1', email: 'ana@example.com', role: 'admin' };
const CUSTOMER = { id: 'c1', email: 'c1@example.com', role: 'customer' };

/**
 * Minimal app: reporting routes + mocked authenticator (no DB).
 * Mirrors build-app.ts error mapping + serializer so AppError(400/403) are
 * surfaced with their real status codes (routes throw AppError; build-app maps it).
 */
async function buildApp(authenticatedUser: unknown) {
  const app = Fastify();
  created.push(app);
  app.setSerializerCompiler(() => (payload: unknown) => JSON.stringify(payload));
  app.setErrorHandler(async (err, _request, reply) => {
    if (err instanceof AppError) {
      return reply
        .code(err.statusCode)
        .send(
          errorEnvelope(err.statusCode, err.code, err.message, 'test-request-id', err.details),
        );
    }
    return reply
      .code(500)
      .send(errorEnvelope(500, 'INTERNAL_ERROR', 'Internal Server Error', 'test-request-id'));
  });
  const mockPool = {
    query: vi.fn().mockResolvedValue({ rows: [] }),
  } as unknown as pg.Pool;
  const deps = {
    authenticate: vi.fn().mockResolvedValue(authenticatedUser),
    pool: mockPool,
  } as unknown as ReportingRoutesDeps;
  await registerReportingRoutes(app, deps);
  await app.ready();
  return { app };
}

afterEach(async () => {
  for (const app of created) {
    try {
      await app.close();
    } catch {
      /* ignore */
    }
  }
  created.length = 0;
});

describe('GET /reporting/filters/schema (REPORTING_VIEW)', () => {
  it('admin receives the schema contract + own grants (AC2)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: '/reporting/filters/schema' });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body ?? '') as {
      filterSchema: { comparison: { modes: string[] }; dataAvailability: Record<string, string> };
      permissions: { role: string; grants: string[] };
    };
    expect(body.filterSchema.comparison.modes).toEqual([
      'none',
      'previous_equal',
      'previous_calendar',
    ]);
    expect(body.filterSchema.dataAvailability.grossSales).toBe('available');
    expect(body.filterSchema.dataAvailability.netSales).toBe('unavailable');
    expect(body.permissions.role).toBe('admin');
    expect(body.permissions.grants).toContain('REPORTING_VIEW');
    expect(body.permissions.grants).toContain('REPORTING_SALES');
  });

  it('customer is forbidden (403) — reporting is backoffice-only', async () => {
    const { app } = await buildApp(CUSTOMER);
    const res = await app.inject({ method: 'GET', url: '/reporting/filters/schema' });
    expect(res.statusCode).toBe(403);
  });
});

describe('GET /reporting/filters/validate (REPORTING_SALES)', () => {
  const BASE =
    '/reporting/filters/validate?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z';

  it('validates a real query and computes the comparison range (AC3)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: `${BASE}&compare=previous_equal` });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body ?? '') as {
      filters: { range: { from: string; to: string }; compare: string };
      comparison: { range: { from: string; to: string } | null };
    };
    const start = new Date(body.filters.range.from).getTime();
    const end = new Date(body.filters.range.to).getTime();
    expect(body.filters.compare).toBe('previous_equal');
    expect(body.comparison.range).not.toBeNull();
    // previous_equal: prior window ends exactly at this range's start (same duration).
    expect(new Date(body.comparison.range!.to).getTime()).toBe(start);
    expect(new Date(body.comparison.range!.from).getTime()).toBeLessThan(start);
    expect(end - start).toBe(
      new Date(body.comparison.range!.to).getTime() - new Date(body.comparison.range!.from).getTime(),
    );
  });

  it('defaults to compare=none and a null range when omitted (AC1/AC3)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: BASE });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body ?? '') as {
      filters: { compare: string };
      comparison: { range: unknown };
    };
    expect(body.filters.compare).toBe('none');
    expect(body.comparison.range).toBeNull();
  });

  it('returns 400 on inverted range (AC1)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({
      method: 'GET',
      url: '/reporting/filters/validate?from=2026-08-31T00:00:00Z&to=2026-08-01T00:00:00Z',
    });
    expect(res.statusCode).toBe(400);
  });

  it('returns 400 on missing required from/to (AC1)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: '/reporting/filters/validate' });
    expect(res.statusCode).toBe(400);
  });

  it('customer is forbidden even with a valid query (RBAC enforced via HTTP, AC5)', async () => {
    const { app } = await buildApp(CUSTOMER);
    const res = await app.inject({ method: 'GET', url: BASE });
    expect(res.statusCode).toBe(403);
  });
});

// ── F-146: Summary and Sales endpoint tests ──────────────────────────────

describe('GET /reporting/summary (REPORTING_SALES)', () => {
  const BASE = '/reporting/summary?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z';

  it('returns summary DTO with all required fields (AC1)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: BASE });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body ?? '') as Record<string, unknown>;
    expect(body).toHaveProperty('range');
    expect(body).toHaveProperty('filters');
    expect(body).toHaveProperty('comparison');
    expect(body).toHaveProperty('dataAvailability');
    expect(body).toHaveProperty('totals');
    expect(body).toHaveProperty('updatedAt');
    expect(body).toHaveProperty('cache');
  });

  it('returns correct dataAvailability flags (AC5)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: BASE });

    const body = JSON.parse(res.body ?? '') as {
      dataAvailability: Record<string, string>;
    };
    expect(body.dataAvailability.grossSales).toBe('available');
    expect(body.dataAvailability.netSales).toBe('unavailable');
    expect(body.dataAvailability.margin).toBe('unavailable');
    expect(body.dataAvailability.paymentMethod).toBe('unavailable');
    expect(body.dataAvailability.shipping).toBe('available');
    expect(body.dataAvailability.discounts).toBe('available');
  });

  it('returns 400 on inverted dates (AC7)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({
      method: 'GET',
      url: '/reporting/summary?from=2026-08-31T00:00:00Z&to=2026-08-01T00:00:00Z',
    });
    expect(res.statusCode).toBe(400);
  });

  it('customer is forbidden (RBAC, AC8)', async () => {
    const { app } = await buildApp(CUSTOMER);
    const res = await app.inject({ method: 'GET', url: BASE });
    expect(res.statusCode).toBe(403);
  });
});

describe('GET /reporting/sales (REPORTING_SALES)', () => {
  const BASE = '/reporting/sales?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z';

  it('returns sales DTO with items and pagination (AC2)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: `${BASE}&groupBy=day` });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body ?? '') as Record<string, unknown>;
    expect(body).toHaveProperty('range');
    expect(body).toHaveProperty('filters');
    expect(body).toHaveProperty('items');
    expect(body).toHaveProperty('totals');
    expect(body).toHaveProperty('pagination');
    expect(body).toHaveProperty('updatedAt');
    expect(Array.isArray((body as Record<string, unknown>).items)).toBe(true);
  });

  it('pagination fields present (AC4)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({ method: 'GET', url: `${BASE}&groupBy=month&page=2&pageSize=20` });

    const body = JSON.parse(res.body ?? '') as {
      pagination: { page: number; pageSize: number; totalRows: number };
    };
    expect(body.pagination.page).toBe(2);
    expect(body.pagination.pageSize).toBe(20);
  });

  it('returns 400 on invalid channel (AC7)', async () => {
    const { app } = await buildApp(ADMIN);
    const res = await app.inject({
      method: 'GET',
      url: '/reporting/sales?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z&channel=invalid',
    });
    expect(res.statusCode).toBe(400);
  });

  it('customer is forbidden (RBAC, AC8)', async () => {
    const { app } = await buildApp(CUSTOMER);
    const res = await app.inject({ method: 'GET', url: BASE });
    expect(res.statusCode).toBe(403);
  });
});
