/**
 * Unit tests for src/controllers/DIDController.ts
 * DIDService is fully mocked. Handlers are invoked with mock req/res/next.
 */

import { Request, Response, NextFunction } from 'express';
import { DIDController } from '../../../src/controllers/DIDController';
import { DIDService } from '../../../src/services/DIDService';
import { AgentRepository } from '../../../src/repositories/AgentRepository';
import { AgentNotFoundError } from '../../../src/utils/errors';
import { IDIDDocument, IDIDResolutionResult, IAgentCard } from '../../../src/types/did';

// ─── Mocks ────────────────────────────────────────────────────────────────────

jest.mock('../../../src/services/DIDService');
jest.mock('../../../src/repositories/AgentRepository');

const MockDIDService = DIDService as jest.MockedClass<typeof DIDService>;
const MockAgentRepository = AgentRepository as jest.MockedClass<typeof AgentRepository>;

// ─── Fixtures ─────────────────────────────────────────────────────────────────

const AGENT_ID = 'agt_test_ctrl';
const TEST_DOMAIN = 'test.example.com';
const INSTANCE_DID = `did:web:${TEST_DOMAIN}`;
const AGENT_DID = `did:web:${TEST_DOMAIN}:agents:${AGENT_ID}`;

const MOCK_INSTANCE_DOC: IDIDDocument = {
  '@context': ['https://www.w3.org/ns/did/v1'],
  id: INSTANCE_DID,
  controller: INSTANCE_DID,
  verificationMethod: [
    {
      id: `${INSTANCE_DID}#keys-1`,
      type: 'JsonWebKey2020',
      controller: INSTANCE_DID,
      publicKeyJwk: { kty: 'EC', crv: 'P-256', use: 'sig' },
    },
  ],
  authentication: [`${INSTANCE_DID}#keys-1`],
};

const MOCK_AGENT_DOC: IDIDDocument = {
  '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/agntcy/v1'],
  id: AGENT_DID,
  controller: INSTANCE_DID,
  verificationMethod: [],
  authentication: [],
};

const MOCK_RESOLUTION_RESULT: IDIDResolutionResult = {
  didDocument: MOCK_AGENT_DOC,
  didDocumentMetadata: {
    created: '2026-01-01T00:00:00.000Z',
    updated: '2026-01-02T00:00:00.000Z',
    deactivated: false,
  },
  didResolutionMetadata: {
    contentType: 'application/did+ld+json',
    retrieved: new Date().toISOString(),
  },
};

const MOCK_AGENT_CARD: IAgentCard = {
  did: AGENT_DID,
  name: 'test@example.com',
  agentType: 'orchestrator',
  capabilities: ['task-planning'],
  owner: 'acme',
  version: '1.0.0',
  deploymentEnv: 'production',
  identityProvider: 'https://idp.sentryagent.ai',
  issuedAt: '2026-01-01T00:00:00.000Z',
};

// ─── Request/Response builder ─────────────────────────────────────────────────

function buildMocks(params: Record<string, string> = {}): {
  req: Partial<Request>;
  res: Partial<Response>;
  next: NextFunction;
} {
  const res: Partial<Response> = {
    status: jest.fn().mockReturnThis(),
    json: jest.fn().mockReturnThis(),
    set: jest.fn().mockReturnThis(),
  };
  return {
    req: {
      params,
      body: {},
      query: {},
      headers: {},
    },
    res,
    next: jest.fn() as NextFunction,
  };
}

// ─── Suite ────────────────────────────────────────────────────────────────────

describe('DIDController', () => {
  let didService: jest.Mocked<DIDService>;
  let agentRepo: jest.Mocked<AgentRepository>;
  let controller: DIDController;

  beforeEach(() => {
    jest.clearAllMocks();
    didService = new MockDIDService({} as never, null, {} as never) as jest.Mocked<DIDService>;
    agentRepo = new MockAgentRepository({} as never) as jest.Mocked<AgentRepository>;
    controller = new DIDController(didService, agentRepo);
  });

  // ─── getInstanceDIDDocument ──────────────────────────────────────────────

  describe('getInstanceDIDDocument()', () => {
    it('should return 200 with the instance DID Document', async () => {
      didService.buildInstanceDIDDocument.mockResolvedValueOnce(MOCK_INSTANCE_DOC);
      const { req, res, next } = buildMocks();

      await controller.getInstanceDIDDocument(req as Request, res as Response, next);

      expect(res.json).toHaveBeenCalledWith(MOCK_INSTANCE_DOC);
      expect(next).not.toHaveBeenCalled();
    });

    it('should call next with the error when DIDService throws', async () => {
      const err = new Error('DID build failed');
      didService.buildInstanceDIDDocument.mockRejectedValueOnce(err);
      const { req, res, next } = buildMocks();

      await controller.getInstanceDIDDocument(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(err);
      expect(res.json).not.toHaveBeenCalled();
    });

    it('should not include private key material in the response', async () => {
      didService.buildInstanceDIDDocument.mockResolvedValueOnce(MOCK_INSTANCE_DOC);
      const { req, res, next } = buildMocks();

      await controller.getInstanceDIDDocument(req as Request, res as Response, next);

      const callArg = (res.json as jest.Mock).mock.calls[0][0] as IDIDDocument;
      const serialised = JSON.stringify(callArg);
      expect(serialised).not.toContain('privateKeyPem');
      expect(serialised).not.toContain('PRIVATE KEY');
    });
  });

  // ─── getAgentDIDDocument ─────────────────────────────────────────────────

  describe('getAgentDIDDocument()', () => {
    it('should return 200 with the agent DID Document for an active agent', async () => {
      didService.buildAgentDIDDocument.mockResolvedValueOnce({
        document: MOCK_AGENT_DOC,
        deactivated: false,
        createdAt: new Date(),
        updatedAt: new Date(),
      });
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentDIDDocument(req as Request, res as Response, next);

      expect(res.json).toHaveBeenCalledWith(MOCK_AGENT_DOC);
      expect(res.status).not.toHaveBeenCalled();
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 410 with AGENT_DECOMMISSIONED code for a decommissioned agent', async () => {
      didService.buildAgentDIDDocument.mockResolvedValueOnce({
        document: MOCK_AGENT_DOC,
        deactivated: true,
        createdAt: new Date(),
        updatedAt: new Date(),
      });
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentDIDDocument(req as Request, res as Response, next);

      expect(res.status).toHaveBeenCalledWith(410);
      expect(res.json).toHaveBeenCalledWith(
        expect.objectContaining({ code: 'AGENT_DECOMMISSIONED' }),
      );
      expect(next).not.toHaveBeenCalled();
    });

    it('should call next with AgentNotFoundError when agent does not exist', async () => {
      const err = new AgentNotFoundError(AGENT_ID);
      didService.buildAgentDIDDocument.mockRejectedValueOnce(err);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentDIDDocument(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(err);
      expect(res.json).not.toHaveBeenCalled();
    });

    it('should call next with generic error when DIDService throws', async () => {
      const err = new Error('Unexpected DB error');
      didService.buildAgentDIDDocument.mockRejectedValueOnce(err);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentDIDDocument(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(err);
    });

    it('should not include private key material in any response', async () => {
      didService.buildAgentDIDDocument.mockResolvedValueOnce({
        document: MOCK_AGENT_DOC,
        deactivated: false,
        createdAt: new Date(),
        updatedAt: new Date(),
      });
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentDIDDocument(req as Request, res as Response, next);

      const callArg = (res.json as jest.Mock).mock.calls[0][0] as IDIDDocument;
      const serialised = JSON.stringify(callArg);
      expect(serialised).not.toContain('privateKeyPem');
      expect(serialised).not.toContain('PRIVATE KEY');
    });
  });

  // ─── resolveAgentDID ─────────────────────────────────────────────────────

  describe('resolveAgentDID()', () => {
    it('should return 200 with the DID Resolution result', async () => {
      didService.buildResolutionResult.mockResolvedValueOnce(MOCK_RESOLUTION_RESULT);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.resolveAgentDID(req as Request, res as Response, next);

      expect(res.json).toHaveBeenCalledWith(MOCK_RESOLUTION_RESULT);
      expect(next).not.toHaveBeenCalled();
    });

    it('should set Content-Type to application/ld+json with DID resolution profile', async () => {
      didService.buildResolutionResult.mockResolvedValueOnce(MOCK_RESOLUTION_RESULT);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.resolveAgentDID(req as Request, res as Response, next);

      expect(res.set).toHaveBeenCalledWith(
        'Content-Type',
        'application/ld+json;profile="https://w3id.org/did-resolution"',
      );
    });

    it('should call next with AgentNotFoundError when agent does not exist', async () => {
      const err = new AgentNotFoundError(AGENT_ID);
      didService.buildResolutionResult.mockRejectedValueOnce(err);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.resolveAgentDID(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(err);
    });

    it('should call next with error when DIDService throws unexpectedly', async () => {
      const err = new Error('Redis connection lost');
      didService.buildResolutionResult.mockRejectedValueOnce(err);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.resolveAgentDID(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(err);
      expect(res.json).not.toHaveBeenCalled();
    });

    it('should not include private key material in the resolution response', async () => {
      didService.buildResolutionResult.mockResolvedValueOnce(MOCK_RESOLUTION_RESULT);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.resolveAgentDID(req as Request, res as Response, next);

      const callArg = (res.json as jest.Mock).mock.calls[0][0] as IDIDResolutionResult;
      const serialised = JSON.stringify(callArg);
      expect(serialised).not.toContain('privateKeyPem');
      expect(serialised).not.toContain('PRIVATE KEY');
    });
  });

  // ─── getAgentCard ─────────────────────────────────────────────────────────

  describe('getAgentCard()', () => {
    it('should return 200 with the AGNTCY agent card', async () => {
      didService.buildAgentCard.mockResolvedValueOnce(MOCK_AGENT_CARD);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentCard(req as Request, res as Response, next);

      expect(res.json).toHaveBeenCalledWith(MOCK_AGENT_CARD);
      expect(next).not.toHaveBeenCalled();
    });

    it('should call next with AgentNotFoundError when agent does not exist', async () => {
      const err = new AgentNotFoundError(AGENT_ID);
      didService.buildAgentCard.mockRejectedValueOnce(err);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentCard(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(err);
      expect(res.json).not.toHaveBeenCalled();
    });

    it('should call next with generic error when DIDService throws', async () => {
      const err = new Error('Service unavailable');
      didService.buildAgentCard.mockRejectedValueOnce(err);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentCard(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(err);
    });

    it('should not include private key material in the agent card response', async () => {
      didService.buildAgentCard.mockResolvedValueOnce(MOCK_AGENT_CARD);
      const { req, res, next } = buildMocks({ agentId: AGENT_ID });

      await controller.getAgentCard(req as Request, res as Response, next);

      const callArg = (res.json as jest.Mock).mock.calls[0][0] as IAgentCard;
      const serialised = JSON.stringify(callArg);
      expect(serialised).not.toContain('privateKeyPem');
      expect(serialised).not.toContain('PRIVATE KEY');
    });
  });
});
