import { Test, TestingModule } from '@nestjs/testing';
import { AiConfigController } from './ai-config.controller';
import { AiConfigService } from './ai-config.service';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { AiProvider } from './ai-config.entity';

describe('AiConfigController', () => {
  let controller: AiConfigController;
  let service: jest.Mocked<Pick<AiConfigService, 'getConfig' | 'saveConfig' | 'testConnection' | 'clearKey'>>;
  let opLog: jest.Mocked<Pick<OperationLogsService, 'log'>>;

  const mockConfig = {
    id: 1,
    provider: AiProvider.OPENAI,
    baseUrl: 'https://api.openai.com/v1',
    hasApiKey: true,
    hasDatabaseKey: true,
    maskedApiKey: '••••1234',
    keySource: 'database' as const,
    defaultModel: 'gpt-4',
    enabled: true,
    timeoutMs: 30000,
    verified: true,
    lastTestedAt: '2024-01-01T00:00:00.000Z',
    lastTestLatencyMs: 250,
    createdAt: '2024-01-01T00:00:00.000Z',
    updatedAt: '2024-01-01T00:00:00.000Z',
  };

  const mockReq = {
    user: { id: 1, username: 'admin' },
    headers: { 'user-agent': 'test', 'x-forwarded-for': '1.2.3.4' },
    connection: { remoteAddress: '1.2.3.4' },
  };

  beforeEach(async () => {
    service = {
      getConfig: jest.fn(),
      saveConfig: jest.fn(),
      testConnection: jest.fn(),
      clearKey: jest.fn(),
    };

    opLog = {
      log: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      controllers: [AiConfigController],
      providers: [
        { provide: AiConfigService, useValue: service },
        { provide: OperationLogsService, useValue: opLog },
      ],
    }).compile();

    controller = module.get<AiConfigController>(AiConfigController);
  });

  // ── GET /ai/config ────────────────────────────────────────────────────

  describe('GET /ai/config', () => {
    it('returns config with success wrapper', async () => {
      service.getConfig.mockResolvedValue(mockConfig);
      const result = await controller.getConfig();
      expect(result.success).toBe(true);
      expect(result.data).toEqual(mockConfig);
    });

    it('calls service.getConfig', async () => {
      service.getConfig.mockResolvedValue(mockConfig);
      await controller.getConfig();
      expect(service.getConfig).toHaveBeenCalledTimes(1);
    });
  });

  // ── PUT /ai/config ────────────────────────────────────────────────────

  describe('PUT /ai/config', () => {
    const saveDto = {
      provider: AiProvider.OPENAI,
      apiKey: 'sk-new-key',
      enabled: true,
    };

    it('saves config and logs operation', async () => {
      const saved = { id: 1, baseUrl: 'https://api.openai.com/v1', ...saveDto } as any;
      service.saveConfig.mockResolvedValue(saved);

      const result = await controller.saveConfig(saveDto, mockReq);

      expect(result.success).toBe(true);
      expect(opLog.log).toHaveBeenCalledWith(
        expect.objectContaining({
          module: 'ai-config',
          action: 'save',
          userId: 1,
          username: 'admin',
          detail: expect.stringContaining('provider=OPENAI'),
        }),
      );
    });

    it('operation log detail does NOT contain apiKey', async () => {
      const saved = { id: 1, baseUrl: 'https://api.openai.com/v1', ...saveDto } as any;
      service.saveConfig.mockResolvedValue(saved);

      await controller.saveConfig(saveDto, mockReq);

      const logCall = opLog.log.mock.calls[0][0];
      expect(logCall.detail).not.toContain('sk-new-key');
      expect(logCall.detail).not.toContain(saveDto.apiKey);
    });

    it('operation log detail does NOT contain full baseUrl', async () => {
      const saved = { id: 1, baseUrl: 'https://api.openai.com/v1', ...saveDto } as any;
      service.saveConfig.mockResolvedValue(saved);

      await controller.saveConfig(saveDto, mockReq);

      const logCall = opLog.log.mock.calls[0][0];
      expect(logCall.detail).not.toContain('api.openai.com/v1');
      // Only hostname should be present
      expect(logCall.detail).toContain('host=api.openai.com');
    });

    it('operation log detail logs model as configured/not-set not raw value', async () => {
      const saved = { id: 1, baseUrl: 'https://api.openai.com/v1', ...saveDto } as any;
      service.saveConfig.mockResolvedValue(saved);

      await controller.saveConfig(
        { provider: AiProvider.OPENAI, defaultModel: 'gpt-4' },
        mockReq,
      );

      const logCall = opLog.log.mock.calls[0][0];
      expect(logCall.detail).toContain('model=configured');
      expect(logCall.detail).not.toContain('gpt-4');
    });

    it('operation log detail logs model=not-set when no defaultModel', async () => {
      const saved = { id: 1, baseUrl: 'https://api.openai.com/v1' } as any;
      service.saveConfig.mockResolvedValue(saved);

      await controller.saveConfig(
        { provider: AiProvider.OPENAI },
        mockReq,
      );

      const logCall = opLog.log.mock.calls[0][0];
      expect(logCall.detail).toContain('model=not-set');
    });
  });

  // ── POST /ai/config/test ──────────────────────────────────────────────

  describe('POST /ai/config/test', () => {
    const testDto = { provider: AiProvider.OPENAI };
    const testResult = {
      success: true,
      latencyMs: 200,
      modelCount: 5,
      modelAvailable: true,
      testedAt: '2024-01-01T00:00:00.000Z',
      message: '连接成功',
    };

    it('returns test result and logs operation', async () => {
      service.testConnection.mockResolvedValue(testResult);

      const result = await controller.testConnection(testDto, mockReq);

      expect(result).toEqual(testResult);
      expect(opLog.log).toHaveBeenCalledWith(
        expect.objectContaining({
          module: 'ai-config',
          action: 'test',
          detail: expect.stringContaining('success=true'),
          status: 'success',
        }),
      );
    });

    it('logs status=failure on test failure', async () => {
      const failResult = { ...testResult, success: false, message: '认证失败' };
      service.testConnection.mockResolvedValue(failResult);

      await controller.testConnection(testDto, mockReq);

      expect(opLog.log).toHaveBeenCalledWith(
        expect.objectContaining({
          status: 'failure',
        }),
      );
    });

    it('operation log detail does NOT contain sensitive info', async () => {
      const dtoWithKey = { provider: AiProvider.OPENAI, apiKey: 'sk-secret-key' };
      service.testConnection.mockResolvedValue(testResult);

      await controller.testConnection(dtoWithKey, mockReq);

      const logCall = opLog.log.mock.calls[0][0];
      expect(logCall.detail).not.toContain('sk-secret-key');
      expect(logCall.detail).not.toContain('Authorization');
    });

    it('operation log detail does NOT contain modelCount', async () => {
      service.testConnection.mockResolvedValue(testResult);

      await controller.testConnection(testDto, mockReq);

      const logCall = opLog.log.mock.calls[0][0];
      expect(logCall.detail).not.toContain('modelCount');
    });
  });

  // ── POST /ai/config/clear-key ─────────────────────────────────────────

  describe('POST /ai/config/clear-key', () => {
    it('clears key and logs operation', async () => {
      service.clearKey.mockResolvedValue({
        ...mockConfig,
        hasApiKey: false,
        hasDatabaseKey: false,
        maskedApiKey: null,
        keySource: 'none',
      });

      const result = await controller.clearKey(mockReq);

      expect(result.success).toBe(true);
      expect(result.data.keySource).toBe('none');
      expect(result.data.hasDatabaseKey).toBe(false);
      expect(opLog.log).toHaveBeenCalledWith(
        expect.objectContaining({
          module: 'ai-config',
          action: 'clear-key',
        }),
      );
    });
  });

  // ── Permission decorators ─────────────────────────────────────────────

  describe('route permissions', () => {
    it('GET /ai/config requires ai:config:read', () => {
      const permissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.getConfig,
      );
      expect(permissions).toContain('ai:config:read');
    });

    it('PUT /ai/config requires ai:config:write', () => {
      const permissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.saveConfig,
      );
      expect(permissions).toContain('ai:config:write');
    });

    it('POST /ai/config/test requires ai:config:test', () => {
      const permissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.testConnection,
      );
      expect(permissions).toContain('ai:config:test');
    });

    it('POST /ai/config/clear-key requires ai:config:write', () => {
      const permissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.clearKey,
      );
      expect(permissions).toContain('ai:config:write');
    });

    it('read permission cannot write', () => {
      const getPermissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.getConfig,
      );
      const savePermissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.saveConfig,
      );
      expect(getPermissions).not.toEqual(savePermissions);
    });

    it('write and test permissions are distinct', () => {
      const writePermissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.saveConfig,
      );
      const testPermissions = Reflect.getMetadata(
        'permissions',
        AiConfigController.prototype.testConnection,
      );
      expect(writePermissions).not.toEqual(testPermissions);
    });
  });
});
