import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import Fastify from 'fastify';
import type { FastifyInstance } from 'fastify';
import { registerAuditEventRoutes } from '../src/routes/audit-events.js';
import { AuditEventService } from '../src/services/audit-event.service.js';
import { errorHandler } from '../src/middleware/error-handler.js';
import type { IAuditEventRepository, AuditEventFilter } from '../src/repositories/interfaces.js';

function mockRepo(): IAuditEventRepository {
  return {
    findAll: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    createMany: vi.fn(async (events: unknown[]) => events.length),
    count: vi.fn(async () => 0),
    listSessions: vi.fn(async () => []),
    countSessions: vi.fn(async () => 0),
  };
}

function makeEvent(overrides: Record<string, unknown> = {}) {
  return {
    id: 'evt-1',
    timestamp: new Date('2026-03-01T12:00:00Z'),
    sessionId: 'sess-1',
    projectName: 'ha-project',
    eventKind: 'gate_decision',
    source: 'mcplocal',
    verified: false,
    serverName: null,
    correlationId: null,
    parentEventId: null,
    payload: { trigger: 'begin_session' },
    createdAt: new Date(),
    ...overrides,
  };
}

describe('audit event routes', () => {
  let app: FastifyInstance;
  let repo: ReturnType<typeof mockRepo>;
  let service: AuditEventService;

  beforeEach(async () => {
    app = Fastify();
    app.setErrorHandler(errorHandler);
    repo = mockRepo();
    service = new AuditEventService(repo);
    registerAuditEventRoutes(app, service);
    await app.ready();
  });

  afterEach(async () => {
    await app.close();
  });

  describe('POST /api/v1/audit/events', () => {
    it('inserts batch of events', async () => {
      const events = [
        { timestamp: '2026-03-01T12:00:00Z', sessionId: 's1', projectName: 'p1', eventKind: 'gate_decision', source: 'mcplocal', verified: false, payload: {} },
        { timestamp: '2026-03-01T12:00:01Z', sessionId: 's1', projectName: 'p1', eventKind: 'stage_execution', source: 'mcplocal', verified: true, payload: {} },
        { timestamp: '2026-03-01T12:00:02Z', sessionId: 's1', projectName: 'p1', eventKind: 'pipeline_execution', source: 'mcplocal', verified: true, payload: {} },
      ];

      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/audit/events',
        payload: events,
      });

      expect(res.statusCode).toBe(201);
      expect(JSON.parse(res.payload)).toEqual({ inserted: 3 });
      expect(repo.createMany).toHaveBeenCalledTimes(1);
    });

    it('rejects invalid event (missing eventKind)', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/audit/events',
        payload: [{ sessionId: 'x', projectName: 'p', source: 'mcplocal', timestamp: '2026-03-01T00:00:00Z' }],
      });

      expect(res.statusCode).toBe(400);
    });

    it('rejects empty array', async () => {
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/audit/events',
        payload: [],
      });

      expect(res.statusCode).toBe(400);
    });
  });

  describe('GET /api/v1/audit/events', () => {
    it('returns events filtered by sessionId', async () => {
      vi.mocked(repo.findAll).mockResolvedValue([makeEvent()]);
      vi.mocked(repo.count).mockResolvedValue(1);

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/audit/events?sessionId=s1',
      });

      expect(res.statusCode).toBe(200);
      const body = JSON.parse(res.payload);
      expect(body.events).toHaveLength(1);
      expect(body.total).toBe(1);
    });

    it('returns events filtered by projectName and eventKind', async () => {
      vi.mocked(repo.findAll).mockResolvedValue([]);
      vi.mocked(repo.count).mockResolvedValue(0);

      await app.inject({
        method: 'GET',
        url: '/api/v1/audit/events?projectName=ha&eventKind=gate_decision',
      });

      const call = vi.mocked(repo.findAll).mock.calls[0]![0] as AuditEventFilter;
      expect(call.projectName).toBe('ha');
      expect(call.eventKind).toBe('gate_decision');
    });

    it('supports time range filtering', async () => {
      vi.mocked(repo.findAll).mockResolvedValue([]);
      vi.mocked(repo.count).mockResolvedValue(0);

      await app.inject({
        method: 'GET',
        url: '/api/v1/audit/events?from=2026-03-01&to=2026-03-02',
      });

      const call = vi.mocked(repo.findAll).mock.calls[0]![0] as AuditEventFilter;
      expect(call.from).toEqual(new Date('2026-03-01'));
      expect(call.to).toEqual(new Date('2026-03-02'));
    });

    it('paginates with limit and offset', async () => {
      vi.mocked(repo.findAll).mockResolvedValue([]);
      vi.mocked(repo.count).mockResolvedValue(100);

      await app.inject({
        method: 'GET',
        url: '/api/v1/audit/events?limit=10&offset=20',
      });

      const call = vi.mocked(repo.findAll).mock.calls[0]![0] as AuditEventFilter;
      expect(call.limit).toBe(10);
      expect(call.offset).toBe(20);
    });
  });

  describe('GET /api/v1/audit/events/:id', () => {
    it('returns single event by id', async () => {
      vi.mocked(repo.findById).mockResolvedValue(makeEvent({ id: 'evt-42' }));

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/audit/events/evt-42',
      });

      expect(res.statusCode).toBe(200);
      const body = JSON.parse(res.payload);
      expect(body.id).toBe('evt-42');
    });

    it('returns 404 for missing event', async () => {
      vi.mocked(repo.findById).mockResolvedValue(null);

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/audit/events/nonexistent',
      });

      expect(res.statusCode).toBe(404);
    });
  });

  describe('GET /api/v1/audit/sessions', () => {
    it('returns session summaries with userName', async () => {
      vi.mocked(repo.listSessions).mockResolvedValue([
        {
          sessionId: 'sess-1',
          projectName: 'ha-project',
          userName: 'michal',
          firstSeen: new Date('2026-03-01T12:00:00Z'),
          lastSeen: new Date('2026-03-01T12:05:00Z'),
          eventCount: 5,
          eventKinds: ['gate_decision', 'tool_call_trace'],
        },
      ]);
      vi.mocked(repo.countSessions).mockResolvedValue(1);

      const res = await app.inject({
        method: 'GET',
        url: '/api/v1/audit/sessions',
      });

      expect(res.statusCode).toBe(200);
      const body = JSON.parse(res.payload);
      expect(body.sessions).toHaveLength(1);
      expect(body.sessions[0].sessionId).toBe('sess-1');
      expect(body.sessions[0].eventCount).toBe(5);
      expect(body.sessions[0].userName).toBe('michal');
      expect(body.total).toBe(1);
    });

    it('filters by projectName', async () => {
      vi.mocked(repo.listSessions).mockResolvedValue([]);
      vi.mocked(repo.countSessions).mockResolvedValue(0);

      await app.inject({
        method: 'GET',
        url: '/api/v1/audit/sessions?projectName=ha-project',
      });

      const call = vi.mocked(repo.listSessions).mock.calls[0]![0] as { projectName?: string };
      expect(call.projectName).toBe('ha-project');
    });

    it('filters by userName', async () => {
      vi.mocked(repo.listSessions).mockResolvedValue([]);
      vi.mocked(repo.countSessions).mockResolvedValue(0);

      await app.inject({
        method: 'GET',
        url: '/api/v1/audit/sessions?userName=michal',
      });

      const call = vi.mocked(repo.listSessions).mock.calls[0]![0] as { userName?: string };
      expect(call.userName).toBe('michal');
    });

    it('filters by date range (from/to)', async () => {
      vi.mocked(repo.listSessions).mockResolvedValue([]);
      vi.mocked(repo.countSessions).mockResolvedValue(0);

      await app.inject({
        method: 'GET',
        url: '/api/v1/audit/sessions?from=2026-03-01&to=2026-03-02',
      });

      const call = vi.mocked(repo.listSessions).mock.calls[0]![0] as { from?: Date; to?: Date };
      expect(call.from).toEqual(new Date('2026-03-01'));
      expect(call.to).toEqual(new Date('2026-03-02'));
    });

    it('supports pagination', async () => {
      vi.mocked(repo.listSessions).mockResolvedValue([]);
      vi.mocked(repo.countSessions).mockResolvedValue(10);

      await app.inject({
        method: 'GET',
        url: '/api/v1/audit/sessions?limit=5&offset=5',
      });

      const call = vi.mocked(repo.listSessions).mock.calls[0]![0] as { limit?: number; offset?: number };
      expect(call.limit).toBe(5);
      expect(call.offset).toBe(5);
    });
  });
});
