/**
 * Unit tests for management-api-client module
 */
import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test';
import { ManagementApiClient } from '../../../src/cliproxy/management-api-client';
import type {
  ManagementClientConfig,
  ManagementHealthStatus,
  ClaudeKey,
} from '../../../src/cliproxy/management-api-types';

describe('management-api-client', () => {
  describe('ManagementApiClient', () => {
    let config: ManagementClientConfig;

    beforeEach(() => {
      config = {
        host: 'localhost',
        port: 8317,
        protocol: 'http',
        managementKey: 'test-management-key-12345',
        timeout: 2000,
        allowSelfSigned: false,
      };
    });

    describe('constructor', () => {
      it('should create client with provided config', () => {
        const client = new ManagementApiClient(config);
        expect(client).toBeDefined();
        expect(client.getBaseUrl()).toBe('http://localhost:8317');
      });

      it('should use default timeout if not provided', () => {
        const configWithoutTimeout = { ...config };
        delete configWithoutTimeout.timeout;
        const client = new ManagementApiClient(configWithoutTimeout);
        expect(client).toBeDefined();
      });
    });

    describe('getBaseUrl', () => {
      it('should construct HTTP URL with port', () => {
        const client = new ManagementApiClient(config);
        expect(client.getBaseUrl()).toBe('http://localhost:8317');
      });

      it('should construct HTTPS URL with custom port', () => {
        const httpsConfig = { ...config, protocol: 'https' as const, port: 8443 };
        const client = new ManagementApiClient(httpsConfig);
        expect(client.getBaseUrl()).toBe('https://localhost:8443');
      });

      it('should omit standard HTTP port 80', () => {
        const configPort80 = { ...config, port: 80 };
        const client = new ManagementApiClient(configPort80);
        expect(client.getBaseUrl()).toBe('http://localhost');
      });

      it('should omit standard HTTPS port 443', () => {
        const configPort443 = { ...config, protocol: 'https' as const, port: 443 };
        const client = new ManagementApiClient(configPort443);
        expect(client.getBaseUrl()).toBe('https://localhost');
      });

      it('should use default port 8317 for HTTP when port is undefined', () => {
        const configNoPort = { ...config };
        delete configNoPort.port;
        const client = new ManagementApiClient(configNoPort);
        expect(client.getBaseUrl()).toBe('http://localhost:8317');
      });

      it('should use default port 443 for HTTPS when port is undefined', () => {
        const configNoPort = { ...config, protocol: 'https' as const };
        delete configNoPort.port;
        const client = new ManagementApiClient(configNoPort);
        expect(client.getBaseUrl()).toBe('https://localhost');
      });

      it('should warn and fall back when configured port is invalid', () => {
        const warnSpy = spyOn(console, 'warn').mockImplementation(() => {});

        const client = new ManagementApiClient({ ...config, port: 99999 });
        expect(client.getBaseUrl()).toBe('http://localhost:8317');
        expect(warnSpy).toHaveBeenCalledWith(
          '[management-api-client] Invalid port "99999", using default 8317'
        );

        warnSpy.mockRestore();
      });
    });

    describe('routing strategy helpers', () => {
      it('reads the routing strategy from the management endpoint', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        const fetchMock = mock(() =>
          Promise.resolve(
            new Response(JSON.stringify({ strategy: 'fill-first' }), {
              status: 200,
              headers: { 'Content-Type': 'application/json' },
            })
          )
        );
        global.fetch = fetchMock as typeof global.fetch;

        const strategy = await client.getRoutingStrategy();

        expect(strategy).toBe('fill-first');
        expect(fetchMock).toHaveBeenCalled();
        global.fetch = originalFetch;
      });

      it('writes the routing strategy using the expected payload shape', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        const fetchMock = mock(() =>
          Promise.resolve(
            new Response(JSON.stringify({ ok: true }), {
              status: 200,
              headers: { 'Content-Type': 'application/json' },
            })
          )
        );
        global.fetch = fetchMock as typeof global.fetch;

        const strategy = await client.putRoutingStrategy('round-robin');

        expect(strategy).toBe('round-robin');
        expect(fetchMock).toHaveBeenCalledWith(
          'http://localhost:8317/routing/strategy',
          expect.objectContaining({
            method: 'PUT',
            body: JSON.stringify({ value: 'round-robin' }),
          })
        );
        global.fetch = originalFetch;
      });
    });

    describe('error code mapping', () => {
      it('should map ENOTFOUND to DNS_FAILED', () => {
        const error = new Error('getaddrinfo ENOTFOUND example.com') as NodeJS.ErrnoException;
        error.code = 'ENOTFOUND';

        // Test via health check which uses mapErrorToCode internally
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() => Promise.reject(error));

        client.health().then((status: ManagementHealthStatus) => {
          expect(status.healthy).toBe(false);
          expect(status.errorCode).toBe('DNS_FAILED');
        });

        global.fetch = originalFetch;
      });

      it('should map ECONNREFUSED to CONNECTION_REFUSED', () => {
        const error = new Error('connect ECONNREFUSED') as NodeJS.ErrnoException;
        error.code = 'ECONNREFUSED';

        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() => Promise.reject(error));

        client.health().then((status: ManagementHealthStatus) => {
          expect(status.healthy).toBe(false);
          expect(status.errorCode).toBe('CONNECTION_REFUSED');
        });

        global.fetch = originalFetch;
      });

      it('should map ETIMEDOUT to TIMEOUT', () => {
        const error = new Error('request timeout') as NodeJS.ErrnoException;
        error.code = 'ETIMEDOUT';

        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() => Promise.reject(error));

        client.health().then((status: ManagementHealthStatus) => {
          expect(status.healthy).toBe(false);
          expect(status.errorCode).toBe('TIMEOUT');
        });

        global.fetch = originalFetch;
      });

      it('should map ENETUNREACH to NETWORK_UNREACHABLE', () => {
        const error = new Error('network unreachable') as NodeJS.ErrnoException;
        error.code = 'ENETUNREACH';

        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() => Promise.reject(error));

        client.health().then((status: ManagementHealthStatus) => {
          expect(status.healthy).toBe(false);
          expect(status.errorCode).toBe('NETWORK_UNREACHABLE');
        });

        global.fetch = originalFetch;
      });

      it('should map 401 status to AUTH_FAILED', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() =>
          Promise.resolve({
            ok: false,
            status: 401,
            statusText: 'Unauthorized',
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response)
        );

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.errorCode).toBe('AUTH_FAILED');

        global.fetch = originalFetch;
      });

      it('should map 403 status to AUTH_FAILED', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() =>
          Promise.resolve({
            ok: false,
            status: 403,
            statusText: 'Forbidden',
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response)
        );

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.errorCode).toBe('AUTH_FAILED');

        global.fetch = originalFetch;
      });

      it('should map 404 status to NOT_FOUND', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() =>
          Promise.resolve({
            ok: false,
            status: 404,
            statusText: 'Not Found',
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response)
        );

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.errorCode).toBe('NOT_FOUND');

        global.fetch = originalFetch;
      });

      it('should map 400 status to BAD_REQUEST', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() =>
          Promise.resolve({
            ok: false,
            status: 400,
            statusText: 'Bad Request',
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response)
        );

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.errorCode).toBe('BAD_REQUEST');

        global.fetch = originalFetch;
      });

      it('should map 500+ status to SERVER_ERROR', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() =>
          Promise.resolve({
            ok: false,
            status: 500,
            statusText: 'Internal Server Error',
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response)
        );

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.errorCode).toBe('SERVER_ERROR');

        global.fetch = originalFetch;
      });

      it('should map unknown errors to UNKNOWN', async () => {
        const client = new ManagementApiClient(config);
        const originalFetch = global.fetch;
        global.fetch = mock(() => Promise.reject(new Error('Something weird happened')));

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.errorCode).toBe('UNKNOWN');

        global.fetch = originalFetch;
      });
    });

    describe('authentication header', () => {
      it('should include Bearer token in Authorization header', async () => {
        const client = new ManagementApiClient(config);
        let capturedHeaders: Record<string, string> = {};

        const originalFetch = global.fetch;
        global.fetch = mock((url: string, options?: RequestInit) => {
          if (options?.headers) {
            capturedHeaders = options.headers as Record<string, string>;
          }
          return Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers(),
            text: () => Promise.resolve('{"claude-api-key":[]}'),
          } as Response);
        });

        await client.getClaudeKeys();
        expect(capturedHeaders['Authorization']).toBe('Bearer test-management-key-12345');

        global.fetch = originalFetch;
      });

      it('should mask management key in logs/errors', () => {
        // Management key should never appear in plain text in error messages
        const sensitiveKey = 'super-secret-key-abc123';
        const clientConfig = { ...config, managementKey: sensitiveKey };
        const client = new ManagementApiClient(clientConfig);

        // The key should be used internally but not exposed
        expect(client.getBaseUrl()).not.toContain(sensitiveKey);
      });
    });

    describe('timeout handling', () => {
      it('should respect custom timeout value', () => {
        const customTimeout = 10000;
        const configWithTimeout = { ...config, timeout: customTimeout };
        const client = new ManagementApiClient(configWithTimeout);
        expect(client).toBeDefined();
      });

      it('should abort request on timeout', async () => {
        const client = new ManagementApiClient({ ...config, timeout: 100 });

        const originalFetch = global.fetch;
        global.fetch = mock(
          () =>
            new Promise((_, reject) => {
              setTimeout(() => {
                const error = new Error('The operation was aborted');
                error.name = 'AbortError';
                reject(error);
              }, 150);
            })
        );

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.errorCode).toBe('TIMEOUT');

        global.fetch = originalFetch;
      });
    });

    describe('self-signed certificate option', () => {
      it('should use fetch for HTTP regardless of allowSelfSigned', async () => {
        const client = new ManagementApiClient({ ...config, allowSelfSigned: true });

        const originalFetch = global.fetch;
        let fetchCalled = false;
        global.fetch = mock(() => {
          fetchCalled = true;
          return Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers(),
            text: () => Promise.resolve('{"claude-api-key":[]}'),
          } as Response);
        });

        await client.getClaudeKeys();
        expect(fetchCalled).toBe(true);

        global.fetch = originalFetch;
      });

      it('should use https module for HTTPS with allowSelfSigned', () => {
        const httpsConfig = {
          ...config,
          protocol: 'https' as const,
          allowSelfSigned: true,
        };
        const client = new ManagementApiClient(httpsConfig);
        expect(client).toBeDefined();
        // Actual HTTPS request would use native https module with rejectUnauthorized: false
      });

      it('should use fetch for HTTPS without allowSelfSigned', async () => {
        const httpsConfig = {
          ...config,
          protocol: 'https' as const,
          allowSelfSigned: false,
        };
        const client = new ManagementApiClient(httpsConfig);

        const originalFetch = global.fetch;
        let fetchCalled = false;
        global.fetch = mock(() => {
          fetchCalled = true;
          return Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers(),
            text: () => Promise.resolve('{"claude-api-key":[]}'),
          } as Response);
        });

        await client.getClaudeKeys();
        expect(fetchCalled).toBe(true);

        global.fetch = originalFetch;
      });
    });

    describe('health check', () => {
      it('should return healthy status with version info', async () => {
        const client = new ManagementApiClient(config);

        const originalFetch = global.fetch;
        global.fetch = mock(() =>
          Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers({
              'x-cpa-version': '1.2.3',
              'x-cpa-commit': 'abc123',
            }),
            text: () => Promise.resolve('{"claude-api-key":[]}'),
          } as Response)
        );

        const status = await client.health();
        expect(status.healthy).toBe(true);
        expect(status.version).toBe('1.2.3');
        expect(status.commit).toBe('abc123');
        expect(status.latencyMs).toBeGreaterThanOrEqual(0);

        global.fetch = originalFetch;
      });

      it('should return unhealthy status on error', async () => {
        const client = new ManagementApiClient(config);

        const originalFetch = global.fetch;
        global.fetch = mock(() => Promise.reject(new Error('Connection failed')));

        const status = await client.health();
        expect(status.healthy).toBe(false);
        expect(status.error).toBeDefined();
        expect(status.errorCode).toBeDefined();

        global.fetch = originalFetch;
      });
    });

    describe('CRUD operations', () => {
      it('should get claude keys', async () => {
        const client = new ManagementApiClient(config);
        const mockKeys: ClaudeKey[] = [{ 'api-key': 'sk-test-123', prefix: 'glm-' }];

        const originalFetch = global.fetch;
        global.fetch = mock(() =>
          Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers(),
            text: () => Promise.resolve(JSON.stringify({ 'claude-api-key': mockKeys })),
          } as Response)
        );

        const keys = await client.getClaudeKeys();
        expect(keys).toEqual(mockKeys);

        global.fetch = originalFetch;
      });

      it('should put claude keys', async () => {
        const client = new ManagementApiClient(config);
        const mockKeys: ClaudeKey[] = [{ 'api-key': 'sk-test-456', prefix: 'kimi-' }];

        const originalFetch = global.fetch;
        let requestBody: string | undefined;
        global.fetch = mock((url: string, options?: RequestInit) => {
          requestBody = options?.body as string;
          return Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response);
        });

        await client.putClaudeKeys(mockKeys);
        expect(requestBody).toBe(JSON.stringify(mockKeys));

        global.fetch = originalFetch;
      });

      it('should patch claude key', async () => {
        const client = new ManagementApiClient(config);
        const patch = {
          index: 0,
          value: { prefix: 'updated-' },
        };

        const originalFetch = global.fetch;
        let requestBody: string | undefined;
        global.fetch = mock((url: string, options?: RequestInit) => {
          requestBody = options?.body as string;
          return Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response);
        });

        await client.patchClaudeKey(patch);
        expect(requestBody).toBe(JSON.stringify(patch));

        global.fetch = originalFetch;
      });

      it('should delete claude key', async () => {
        const client = new ManagementApiClient(config);
        const apiKey = 'sk-test-to-delete';

        const originalFetch = global.fetch;
        let requestUrl = '';
        global.fetch = mock((url: string) => {
          requestUrl = url;
          return Promise.resolve({
            ok: true,
            status: 200,
            headers: new Headers(),
            text: () => Promise.resolve(''),
          } as Response);
        });

        await client.deleteClaudeKey(apiKey);
        expect(requestUrl).toContain(encodeURIComponent(apiKey));

        global.fetch = originalFetch;
      });
    });
  });
});
