import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { createGatewayServers } from '../../apps/gateway/src/server.js';
import { ConfigLoader } from '@zyphon/config-loader';
import { DailyLogWriter, DailyLogReader } from '@zyphon/logger';
import { CredentialVault } from '../../apps/gateway/src/core/credential-vault.js';
import { QuotaManager } from '../../apps/gateway/src/core/quota-manager.js';
import { ApplicationRegistry } from '../../apps/gateway/src/core/application-registry.js';
import { CircuitBreakerManager } from '../../apps/gateway/src/core/circuit-breaker.js';
import { RoutingEngine } from '../../apps/gateway/src/core/routing-engine.js';
import { ProviderRegistry } from '../../apps/gateway/src/core/provider-registry.js';
import { IdempotencyManager } from '../../apps/gateway/src/core/idempotency-manager.js';
import { EventBus } from '../../apps/gateway/src/realtime/event-bus.js';
import { MetricsCollector } from '../../apps/gateway/src/core/metrics.js';
import { DispatchService } from '../../apps/gateway/src/messaging/dispatch-service.js';
import { AdminAuthManager } from '../../apps/gateway/src/auth/admin-auth.js';
import { MockProvider } from '@zyphon/provider-mock';
import { resolveMasterKey } from '@zyphon/crypto';
import type { ApplicationConfig } from '@zyphon/shared-types';

describe('15-Step Dispatch Pipeline Integration', () => {
  let tmpDir: string;
  let servers: ReturnType<typeof createGatewayServers>;
  let quotaManager: QuotaManager;
  let eventBus: EventBus;
  let appRegistry: ApplicationRegistry;
  let mockPrimary: MockProvider;
  let mockBackup: MockProvider;
  let mockSms: MockProvider;
  let testApp: ApplicationConfig;
  let testApiKey: string;
  let logsDir: string;

  beforeEach(async () => {
    tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'zyphon-pipeline-test-'));
    const configDir = path.join(tmpDir, 'config');
    const stateDir = path.join(tmpDir, 'data', 'state');
    logsDir = path.join(tmpDir, 'data', 'logs');

    await fs.mkdir(configDir, { recursive: true });
    await fs.mkdir(stateDir, { recursive: true });
    await fs.mkdir(logsDir, { recursive: true });

    const masterKey = resolveMasterKey('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef');
    const vault = new CredentialVault(path.join(stateDir, 'vault.enc.json'), masterKey);
    quotaManager = new QuotaManager(stateDir);

    testApp = {
      id: 'app_test',
      name: 'Test App',
      enabled: true,
      source: 'manual',
      channels: ['email', 'sms'], // whatsapp intentionally forbidden
      quota: { per_min: 5, per_day: 100 },
      bindings: {},
    };

    appRegistry = new ApplicationRegistry([testApp], quotaManager, stateDir);
    const keyData = appRegistry.generateKey('app_test');
    testApiKey = keyData.api_key;

    const circuitBreaker = new CircuitBreakerManager({ failureThreshold: 2, recoveryTimeSeconds: 10 });
    const routingEngine = new RoutingEngine({
      default: {
        email: ['email-primary', 'email-backup'],
        sms: ['sms-primary'],
        whatsapp: ['whatsapp-primary'],
      },
      retry: { max_attempts: 2, backoff_ms: 10 },
      timeout_ms: { email: 1000, sms: 1000, whatsapp: 1000 },
    }, circuitBreaker);

    mockPrimary = new MockProvider({ id: 'email-primary', channel: 'email' });
    mockBackup = new MockProvider({ id: 'email-backup', channel: 'email' });
    mockSms = new MockProvider({ id: 'sms-primary', channel: 'sms' });

    const providerRegistry = new ProviderRegistry(vault);
    providerRegistry.registerAdapter(mockPrimary);
    providerRegistry.registerAdapter(mockBackup);
    providerRegistry.registerAdapter(mockSms);
    providerRegistry.registerAdapter(new MockProvider({ id: 'whatsapp-primary', channel: 'whatsapp' }));

    const logWriter = new DailyLogWriter({ logsDir });
    const logReader = new DailyLogReader(logsDir);
    eventBus = new EventBus(100);
    const idempotencyManager = new IdempotencyManager(5, 100);
    const metrics = new MetricsCollector();
    const configLoader = new ConfigLoader(configDir, stateDir);

    await fs.writeFile(
      path.join(configDir, 'templates.yaml'),
      [
        'templates:',
        '  - id: tpl-login-code',
        '    name: Login code',
        '    channel: sms',
        '    body: "Your code is {{code}}. It expires in 10 minutes."',
        '    dlt_id: "1107161234567890123"',
        '    header: ACMEIN',
        '    category: otp',
        '    variables: [code]',
        '',
      ].join('\n'),
    );

    const dispatchService = new DispatchService({
      appRegistry,
      routingEngine,
      vault,
      circuitBreaker,
      idempotencyManager,
      providerRegistry,
      logWriter,
      eventBus,
      configLoader,
    });

    const adminAuth = new AdminAuthManager('test-admin-secret');

    servers = createGatewayServers({
      dispatchService,
      appRegistry,
      routingEngine,
      vault,
      circuitBreaker,
      providerRegistry,
      eventBus,
      adminAuth,
      metrics,
      logWriter,
      logReader,
      configLoader,
    });
  });

  afterEach(async () => {
    quotaManager.stop();
    eventBus.stop();
    await servers.publicApp.close();
    await servers.adminApp.close();
    await fs.rm(tmpDir, { recursive: true, force: true });
  });

  it('rejects unauthenticated requests with 401', async () => {
    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      payload: {
        channel: 'email',
        recipient: 'test@example.com',
        content: { type: 'text', body: 'Hello' },
      },
    });

    expect(res.statusCode).toBe(401);
    const body = JSON.parse(res.body);
    expect(body.error.code).toBe('UNAUTHENTICATED');
  });

  it('rejects requests when application is disabled with 403', async () => {
    testApp.enabled = false;
    appRegistry.setApplications([testApp]);

    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'test@example.com',
        content: { type: 'text', body: 'Hello' },
      },
    });

    expect(res.statusCode).toBe(403);
    const body = JSON.parse(res.body);
    expect(body.error.code).toBe('APP_DISABLED');
  });

  it('rejects unauthorized channel with 403 (Fail-Closed channel access)', async () => {
    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'whatsapp', // Not in testApp.channels
        recipient: '+14155552671',
        content: { type: 'text', body: 'Hello WhatsApp' },
      },
    });

    expect(res.statusCode).toBe(403);
    const body = JSON.parse(res.body);
    expect(body.error.code).toBe('CHANNEL_FORBIDDEN');
  });

  it('rejects invalid recipient format with 422', async () => {
    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'not-an-email',
        content: { type: 'text', body: 'Hello' },
      },
    });

    expect(res.statusCode).toBe(422);
    const body = JSON.parse(res.body);
    expect(body.error.code).toBe('INVALID_RECIPIENT');
  });

  it('successfully dispatches message via primary provider (200)', async () => {
    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', subject: 'Integration Test', body: 'Success test body' },
      },
    });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body);
    expect(body.status).toBe('delivered');
    expect(body.provider).toBe('email-primary');
    expect(body.failover).toBe(false);
    expect(mockPrimary.messageLog.length).toBe(1);
  });

  it('renders template variables before SMS dispatch', async () => {
    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'sms',
        recipient: '+14155550100',
        content: {
          type: 'template',
          body: '',
          template: 'tpl-login-code',
          variables: { code: '482913' },
        },
      },
    });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body);
    expect(body.status).toBe('delivered');
    expect(body.provider).toBe('sms-primary');
    expect(mockSms.messageLog.length).toBe(1);
    const sent = mockSms.messageLog[0]!;
    expect(sent.content.body).toContain('482913');
    expect(sent.metadata.dlt_template_id).toBe('1107161234567890123');
    expect(sent.metadata.template_variable_values).toEqual(['482913']);
  });

  it('replays response on identical X-Idempotency-Key with X-Cache: HIT', async () => {
    const idempotencyKey = 'idem_key_unique_123';

    const res1 = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: {
        'X-API-Key': testApiKey,
        'X-Idempotency-Key': idempotencyKey,
      },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', body: 'Idempotency body' },
      },
    });

    expect(res1.statusCode).toBe(200);
    const msgId1 = JSON.parse(res1.body).message_id;

    // Second call with same idempotency key
    const res2 = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: {
        'X-API-Key': testApiKey,
        'X-Idempotency-Key': idempotencyKey,
      },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', body: 'Idempotency body' },
      },
    });

    expect(res2.statusCode).toBe(200);
    expect(res2.headers['x-cache']).toBe('HIT');
    expect(JSON.parse(res2.body).message_id).toBe(msgId1);
  });

  it('fails over to backup provider when primary provider fails', async () => {
    mockPrimary.forcedFailure = true; // Primary fails

    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', body: 'Failover test' },
      },
    });

    expect(res.statusCode).toBe(200);
    const body = JSON.parse(res.body);
    expect(body.status).toBe('delivered');
    expect(body.provider).toBe('email-backup');
    expect(body.failover).toBe(true);
    expect(mockBackup.messageLog.length).toBe(1);
  });

  it('trips circuit breaker when all providers fail', async () => {
    mockPrimary.forcedFailure = true;
    mockBackup.forcedFailure = true;

    const res = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', body: 'All fail test' },
      },
    });

    expect(res.statusCode).toBe(502);
    const body = JSON.parse(res.body);
    expect(body.status).toBe('failed');
    expect(body.error.code).toBe('ALL_PROVIDERS_FAILED');
  });

  it('enforces rate limits per minute (429)', async () => {
    // Limit is 5 per min
    for (let i = 0; i < 5; i++) {
      const res = await servers.publicApp.inject({
        method: 'POST',
        url: '/api/v1/messages',
        headers: { 'X-API-Key': testApiKey },
        payload: {
          channel: 'email',
          recipient: 'john@example.com',
          content: { type: 'text', body: `Rate limit test ${i}` },
        },
      });
      expect(res.statusCode).toBe(200);
    }

    // 6th request must trigger 429
    const blockedRes = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', body: 'Exceeded rate limit' },
      },
    });

    expect(blockedRes.statusCode).toBe(429);
    const body = JSON.parse(blockedRes.body);
    expect(body.error.code).toBe('RATE_LIMITED');
  });

  it('blocks dispatch when client IP is not in allowed_ips', async () => {
    testApp.allowed_ips = ['203.0.113.10'];
    appRegistry.setApplications([testApp]);

    const blockedRes = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', body: 'IP blocked' },
      },
    });
    expect(blockedRes.statusCode).toBe(403);
    expect(JSON.parse(blockedRes.body).error.code).toBe('IP_FORBIDDEN');

    const allowedRes = await servers.publicApp.inject({
      method: 'POST',
      url: '/api/v1/messages',
      remoteAddress: '203.0.113.10',
      headers: { 'X-API-Key': testApiKey },
      payload: {
        channel: 'email',
        recipient: 'john@example.com',
        content: { type: 'text', body: 'IP allowed' },
      },
    });
    expect(allowedRes.statusCode).toBe(200);
  });
});

