/**
 * Smoke tests: Audit event end-to-end.
 *
 * Validates that gate decisions and pipeline executions produce audit events
 * in mcpd. Requires a running mcplocal + mcpd with the smoke-data project.
 *
 * Run with: pnpm test:smoke
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import http from 'node:http';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { SmokeMcpSession, isMcplocalRunning, getMcpdUrl, mcpctl } from './mcp-client.js';
import { ChatReporter } from './reporter.js';
import { resolve } from 'node:path';

const PROJECT_NAME = 'smoke-data';
const MCPD_URL = getMcpdUrl();
const FIXTURE_PATH = resolve(import.meta.dirname, 'fixtures', 'smoke-data.yaml');

/** Load auth token and mcpd URL from ~/.mcpctl/credentials. */
function loadMcpdCredentials(): { token: string; url: string } {
  try {
    const raw = readFileSync(join(homedir(), '.mcpctl', 'credentials'), 'utf-8');
    const parsed = JSON.parse(raw) as { token?: string; mcpdUrl?: string };
    return {
      token: parsed.token ?? '',
      url: parsed.mcpdUrl ?? MCPD_URL,
    };
  } catch {
    return { token: '', url: MCPD_URL };
  }
}
const MCPD_CREDS = loadMcpdCredentials();
// Use credentials URL when available (production mcpd), fall back to env/default
const MCPD_EFFECTIVE_URL = MCPD_CREDS.url || MCPD_URL;

interface AuditEvent {
  eventKind: string;
  projectName: string;
  source: string;
  verified: boolean;
  userName?: string | null;
  payload: Record<string, unknown>;
}

interface AuditSession {
  sessionId: string;
  projectName: string;
  userName: string | null;
  firstSeen: string;
  lastSeen: string;
  eventCount: number;
  eventKinds: string[];
}

interface AuditQueryResult {
  events: AuditEvent[];
  total: number;
}

interface AuditSessionResult {
  sessions: AuditSession[];
  total: number;
}

/** Fetch JSON from mcpd REST API (with auth from credentials). Retries on rate limit. */
async function mcpdGet<T>(path: string, retries = 3): Promise<T> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    const result = await new Promise<{ status: number; body: T }>((resolve, reject) => {
      const url = new URL(path, MCPD_EFFECTIVE_URL);
      const headers: Record<string, string> = { 'Accept': 'application/json' };
      if (MCPD_CREDS.token) headers['Authorization'] = `Bearer ${MCPD_CREDS.token}`;
      http.get(url, { timeout: 10_000, headers }, (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunk: Buffer) => chunks.push(chunk));
        res.on('end', () => {
          try {
            resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString('utf-8')) as T });
          } catch (err) {
            reject(err);
          }
        });
      }).on('error', reject);
    });
    if (result.status === 429 && attempt < retries) {
      await new Promise((r) => setTimeout(r, 2_000 * (attempt + 1)));
      continue;
    }
    return result.body;
  }
  throw new Error('mcpdGet: max retries exceeded');
}

/** Query audit events from mcpd. */
async function queryAuditEvents(params: string): Promise<AuditEvent[]> {
  const result = await mcpdGet<AuditQueryResult>(`/api/v1/audit/events?${params}`);
  return result.events ?? [];
}

describe('Smoke: Audit events', () => {
  let available = false;
  let serverResponding = false;

  beforeAll(async () => {
    console.log('');
    console.log('  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('  Smoke Test: Audit Events');
    console.log('  Project: smoke-data');
    console.log('  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

    available = await isMcplocalRunning();
    if (!available) {
      console.log('\n  ✗ mcplocal not running — all tests will be skipped\n');
      return;
    }

    // Ensure fixture data exists
    try {
      await mcpctl(`describe project ${PROJECT_NAME}`);
    } catch {
      console.log('\n  Applying fixture smoke-data.yaml ...');
      try {
        await mcpctl(`apply -f ${FIXTURE_PATH}`);
      } catch (err) {
        console.log(`  ⚠ Fixture apply error: ${err instanceof Error ? err.message : err}`);
      }
    }

    // Verify audit endpoint exists
    try {
      await mcpdGet<AuditQueryResult>(`/api/v1/audit/events?limit=1`);
      console.log('  ✓ Audit endpoint available');
    } catch (err) {
      console.log(`  ✗ Audit endpoint unavailable: ${err instanceof Error ? err.message : err}`);
      console.log('    Audit tests will be skipped');
      return;
    }

    // Preflight MCP connection
    const preflight = new SmokeMcpSession(PROJECT_NAME);
    try {
      await preflight.initialize();
      serverResponding = true;
      console.log('  ✓ Server responding');
    } catch (err) {
      console.log(`  ✗ Server not responding: ${err instanceof Error ? err.message : err}`);
    } finally {
      await preflight.close();
    }
  }, 30_000);

  afterAll(() => {
    console.log('\n  ━━━ Audit smoke tests complete ━━━\n');
  });

  it('gate decision produces audit events after begin_session', async () => {
    if (!serverResponding) return;

    const testStart = new Date().toISOString();
    const session = new SmokeMcpSession(PROJECT_NAME);
    const chat = new ChatReporter(session);
    chat.section('Gate Decision Audit');

    try {
      await chat.initialize();
      const tools = await chat.listTools();
      chat.check('Gated (only begin_session)', tools.length, (v) => v >= 1);

      // Trigger gate decision
      await chat.callTool('begin_session', { description: 'audit smoke test' });

      // Wait for async audit flush (collector batches at 5s or 50 events)
      await new Promise((r) => setTimeout(r, 7_000));

      // Query mcpd for gate_decision events for this project (only from this test run)
      const events = await queryAuditEvents(`projectName=${PROJECT_NAME}&eventKind=gate_decision&limit=5&from=${testStart}`);

      chat.check('Gate decision events exist', events.length, (v) => v >= 1);

      if (events.length > 0) {
        // Find the begin_session event (not auto_intercept from other runs)
        const evt = events.find((e) => e.payload['trigger'] === 'begin_session') ?? events[0]!;
        chat.check('Event kind is gate_decision', evt.eventKind, (v) => v === 'gate_decision');
        chat.check('Source is client', evt.source, (v) => v === 'client');
        chat.check('Verified is false (self-reported)', String(evt.verified), (v) => v === 'false');
        chat.check('Payload has trigger', String(evt.payload['trigger']), (v) => v === 'begin_session');
        chat.check('Payload has clientIntent', String(evt.payload['clientIntent'] != null), (v) => v === 'true');

        expect(evt.eventKind).toBe('gate_decision');
        expect(evt.source).toBe('client');
        expect(evt.verified).toBe(false);
      }
    } finally {
      await chat.close();
    }
  }, 30_000);

  it('tool call produces pipeline audit events', async () => {
    if (!serverResponding) return;

    const testStart = new Date().toISOString();
    const session = new SmokeMcpSession(PROJECT_NAME);
    const chat = new ChatReporter(session);
    chat.section('Pipeline Audit');

    try {
      await chat.initialize();

      // Ungate first
      await chat.callTool('begin_session', { description: 'pipeline audit test' });

      // List tools after ungating
      const tools = await chat.listTools();
      const serverTool = tools.find((t) => t.name.startsWith('smoke-aws-docs/'));
      if (!serverTool) {
        console.log('    No server tools available — skipping pipeline audit test');
        return;
      }

      chat.check('Found server tool', serverTool.name, (v) => !!v);

      // Call a real server tool
      try {
        await chat.callTool(serverTool.name, {}, 20_000);
      } catch {
        // Tool call may fail — that's OK, pipeline still runs
      }

      // Wait for audit flush
      await new Promise((r) => setTimeout(r, 7_000));

      // Query pipeline_execution events (only from this test run)
      const events = await queryAuditEvents(`projectName=${PROJECT_NAME}&eventKind=pipeline_execution&limit=5&from=${testStart}`);

      // Pipeline events may or may not exist depending on whether the tool
      // returned content that triggered the proxymodel pipeline
      if (events.length > 0) {
        const evt = events[0]!;
        chat.check('Pipeline event kind', evt.eventKind, (v) => v === 'pipeline_execution');
        chat.check('Source is mcplocal', evt.source, (v) => v === 'mcplocal');
        chat.check('Verified is true', String(evt.verified), (v) => v === 'true');

        expect(evt.source).toBe('mcplocal');
        expect(evt.verified).toBe(true);
      } else {
        console.log('    No pipeline events (tool may not have returned processable content)');
      }
    } finally {
      await chat.close();
    }
  }, 45_000);

  it('stage_execution events appear for each pipeline stage', async () => {
    if (!serverResponding) return;

    // Wait a moment to avoid rate-limiting the API
    await new Promise((r) => setTimeout(r, 1_000));

    // Query stage events (these should exist from the previous test's tool call)
    const events = await queryAuditEvents(`projectName=${PROJECT_NAME}&eventKind=stage_execution&limit=10`);

    if (events.length > 0) {
      const evt = events[0]!;
      expect(evt.eventKind).toBe('stage_execution');
      expect(evt.payload['stage']).toBeDefined();
      expect(typeof evt.payload['durationMs']).toBe('number');
      expect(typeof evt.payload['inputSize']).toBe('number');
      expect(typeof evt.payload['outputSize']).toBe('number');
      console.log(`    ✓ Found ${events.length} stage_execution events`);
    } else {
      console.log('    No stage events yet (depends on pipeline having run)');
    }
  }, 15_000);

  it('audit events endpoint supports filtering', async () => {
    if (!available) return;

    // Test query returns structured response
    const result = await mcpdGet<AuditQueryResult>(
      `/api/v1/audit/events?projectName=${PROJECT_NAME}&limit=3&offset=0`,
    );

    expect(result).toHaveProperty('events');
    expect(Array.isArray(result.events)).toBe(true);
    expect(result).toHaveProperty('total');
    console.log(`    ✓ Audit API returned ${result.events.length} events (total: ${result.total})`);
  }, 10_000);

  it('sessions endpoint returns userName field', async () => {
    if (!available) return;

    const result = await mcpdGet<AuditSessionResult>(
      `/api/v1/audit/sessions?limit=5`,
    );

    expect(result).toHaveProperty('sessions');
    expect(Array.isArray(result.sessions)).toBe(true);
    if (result.sessions.length > 0) {
      const session = result.sessions[0]!;
      // userName should be present in the response (may be null for old sessions)
      expect(session).toHaveProperty('userName');
      expect(session).toHaveProperty('projectName');
      expect(session).toHaveProperty('eventCount');
      console.log(`    ✓ Session ${session.sessionId.slice(0, 8)} — userName: ${session.userName ?? '(null)'}, events: ${session.eventCount}`);
    } else {
      console.log('    No sessions available');
    }
  }, 10_000);

  it('sessions endpoint supports date range filter', async () => {
    if (!available) return;

    // Query with a very recent "from" — should return fewer sessions
    const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
    const result = await mcpdGet<AuditSessionResult>(
      `/api/v1/audit/sessions?from=${oneHourAgo}&limit=50`,
    );

    expect(result).toHaveProperty('sessions');
    expect(Array.isArray(result.sessions)).toBe(true);
    expect(result).toHaveProperty('total');
    console.log(`    ✓ Sessions in last hour: ${result.sessions.length} (total: ${result.total})`);
  }, 10_000);
});
