/**
 * Unit tests for FederationController (FED-M2-08).
 *
 * Coverage:
 *  - listGrants: delegates to GrantsService with query params
 *  - createGrant: delegates to GrantsService, validates body
 *  - generateToken: returns enrollmentUrl containing the token
 *  - listPeers: returns DB rows
 */

import 'reflect-metadata';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NotFoundException } from '@nestjs/common';
import type { Db } from '@mosaicstack/db';
import { FederationController } from '../federation.controller.js';
import type { GrantsService } from '../grants.service.js';
import type { EnrollmentService } from '../enrollment.service.js';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const GRANT_ID = 'g1111111-1111-1111-1111-111111111111';
const PEER_ID = 'p2222222-2222-2222-2222-222222222222';
const USER_ID = 'u3333333-3333-3333-3333-333333333333';

const MOCK_GRANT = {
  id: GRANT_ID,
  peerId: PEER_ID,
  subjectUserId: USER_ID,
  scope: { resources: ['tasks'], operations: ['list'] },
  status: 'pending' as const,
  expiresAt: null,
  createdAt: new Date('2026-01-01T00:00:00Z'),
  revokedAt: null,
  revokedReason: null,
};

const MOCK_PEER = {
  id: PEER_ID,
  commonName: 'test-peer',
  displayName: 'Test Peer',
  certPem: '',
  certSerial: 'pending',
  certNotAfter: new Date(0),
  clientKeyPem: null,
  state: 'pending' as const,
  endpointUrl: null,
  createdAt: new Date('2026-01-01T00:00:00Z'),
  updatedAt: new Date('2026-01-01T00:00:00Z'),
};

// ---------------------------------------------------------------------------
// DB mock builder
// ---------------------------------------------------------------------------

function makeDbMock(rows: unknown[] = []) {
  const orderBy = vi.fn().mockResolvedValue(rows);
  const where = vi.fn().mockReturnValue({ orderBy });
  const from = vi.fn().mockReturnValue({ where, orderBy });
  const select = vi.fn().mockReturnValue({ from });

  return {
    select,
    from,
    where,
    orderBy,
    insert: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  } as unknown as Db;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('FederationController', () => {
  let db: Db;
  let grantsService: GrantsService;
  let enrollmentService: EnrollmentService;
  let controller: FederationController;

  beforeEach(() => {
    db = makeDbMock([MOCK_PEER]);

    grantsService = {
      createGrant: vi.fn().mockResolvedValue(MOCK_GRANT),
      getGrant: vi.fn().mockResolvedValue(MOCK_GRANT),
      listGrants: vi.fn().mockResolvedValue([MOCK_GRANT]),
      revokeGrant: vi.fn().mockResolvedValue({ ...MOCK_GRANT, status: 'revoked' }),
      activateGrant: vi.fn(),
      expireGrant: vi.fn(),
    } as unknown as GrantsService;

    enrollmentService = {
      createToken: vi.fn().mockResolvedValue({
        token: 'abc123def456abc123def456abc123def456abc123def456abc123def456ab12',
        expiresAt: '2026-01-01T00:15:00.000Z',
      }),
      redeem: vi.fn(),
    } as unknown as EnrollmentService;

    controller = new FederationController(db, grantsService, enrollmentService);
  });

  // ─── Grant management ──────────────────────────────────────────────────

  describe('listGrants', () => {
    it('delegates to GrantsService with provided query params', async () => {
      const query = { peerId: PEER_ID, status: 'pending' as const };
      const result = await controller.listGrants(query);

      expect(grantsService.listGrants).toHaveBeenCalledWith(query);
      expect(result).toEqual([MOCK_GRANT]);
    });

    it('delegates to GrantsService with empty filters', async () => {
      const result = await controller.listGrants({});

      expect(grantsService.listGrants).toHaveBeenCalledWith({});
      expect(result).toEqual([MOCK_GRANT]);
    });
  });

  describe('createGrant', () => {
    it('delegates to GrantsService and returns created grant', async () => {
      const body = {
        peerId: PEER_ID,
        subjectUserId: USER_ID,
        scope: { resources: ['tasks'], operations: ['list'] },
      };

      const result = await controller.createGrant(body);

      expect(grantsService.createGrant).toHaveBeenCalledWith(body);
      expect(result).toEqual(MOCK_GRANT);
    });
  });

  describe('getGrant', () => {
    it('delegates to GrantsService with provided ID', async () => {
      const result = await controller.getGrant(GRANT_ID);

      expect(grantsService.getGrant).toHaveBeenCalledWith(GRANT_ID);
      expect(result).toEqual(MOCK_GRANT);
    });
  });

  describe('revokeGrant', () => {
    it('delegates to GrantsService with id and reason', async () => {
      const result = await controller.revokeGrant(GRANT_ID, { reason: 'test reason' });

      expect(grantsService.revokeGrant).toHaveBeenCalledWith(GRANT_ID, 'test reason');
      expect(result).toMatchObject({ status: 'revoked' });
    });

    it('delegates without reason when omitted', async () => {
      await controller.revokeGrant(GRANT_ID, {});

      expect(grantsService.revokeGrant).toHaveBeenCalledWith(GRANT_ID, undefined);
    });
  });

  describe('generateToken', () => {
    it('returns enrollmentUrl containing the token', async () => {
      const token = 'abc123def456abc123def456abc123def456abc123def456abc123def456ab12';
      vi.mocked(enrollmentService.createToken).mockResolvedValueOnce({
        token,
        expiresAt: '2026-01-01T00:15:00.000Z',
      });

      const result = await controller.generateToken(GRANT_ID, { ttlSeconds: 900 });

      expect(result.token).toBe(token);
      expect(result.enrollmentUrl).toContain(token);
      expect(result.enrollmentUrl).toContain('/api/federation/enrollment/');
    });

    it('creates token via EnrollmentService with correct grantId and peerId', async () => {
      await controller.generateToken(GRANT_ID, { ttlSeconds: 300 });

      expect(enrollmentService.createToken).toHaveBeenCalledWith({
        grantId: GRANT_ID,
        peerId: PEER_ID,
        ttlSeconds: 300,
      });
    });

    it('throws NotFoundException when grant does not exist', async () => {
      vi.mocked(grantsService.getGrant).mockRejectedValueOnce(
        new NotFoundException(`Grant ${GRANT_ID} not found`),
      );

      await expect(controller.generateToken(GRANT_ID, { ttlSeconds: 900 })).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  // ─── Peer management ───────────────────────────────────────────────────

  describe('listPeers', () => {
    it('returns DB rows ordered by commonName', async () => {
      const result = await controller.listPeers();

      expect(db.select).toHaveBeenCalled();
      // The DB mock resolves with [MOCK_PEER]
      expect(result).toEqual([MOCK_PEER]);
    });
  });
});
