/**
 * Smoke tests: ProxyModel pipeline end-to-end.
 *
 * These tests require a running mcplocal + mcpd with real servers deployed.
 * Run with: pnpm test:smoke
 *
 * Prerequisites:
 *   - mcplocal running on localhost:3200
 *   - mcpd running on 10.0.0.194:3100
 *   - smoke-aws-docs server deployed (runtime: python)
 *
 * The test suite uses the fixture at fixtures/smoke-data.yaml which
 * declares the smoke-aws-docs server, smoke-data project, and 100
 * prompt links. `mcpctl apply` is run in beforeAll to ensure the
 * data exists (idempotent).
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { writeFile, mkdir, rm } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { SmokeMcpSession, isMcplocalRunning, mcpctl } from './mcp-client.js';
import { ChatReporter } from './reporter.js';

const PROJECT_NAME = 'smoke-data';
const PROXYMODELS_DIR = join(process.env.HOME ?? '/tmp', '.mcpctl', 'proxymodels');
const FIXTURE_PATH = resolve(import.meta.dirname, 'fixtures', 'smoke-data.yaml');

describe('Smoke: ProxyModel pipeline', () => {
  let available = false;
  /** Set to true after preflight verifies the MCP server actually responds. */
  let serverResponding = false;

  beforeAll(async () => {
    console.log('');
    console.log('  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('  Smoke Test: ProxyModel Pipeline');
    console.log('  Project: smoke-data  Server: smoke-aws-docs');
    console.log('  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

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

    // Check if fixture data already exists; apply only if needed.
    let needsApply = true;
    try {
      const output = await mcpctl(`describe project ${PROJECT_NAME}`);
      if (output.includes('smoke-aws-docs')) {
        console.log('\n  ✓ Fixture data already deployed');
        needsApply = false;
      }
    } catch {
      // Project doesn't exist — need to apply
    }

    if (needsApply) {
      console.log('\n  Applying fixture smoke-data.yaml ...');
      try {
        await mcpctl(`apply -f ${FIXTURE_PATH}`);
        console.log('  ✓ Fixture applied');
      } catch (err) {
        console.log(`  ⚠ Fixture apply error: ${err instanceof Error ? err.message : err}`);
      }
    }

    // Preflight: verify the MCP server actually responds to initialize.
    console.log('\n  Preflight: connecting to smoke-data MCP endpoint...');
    const preflight = new SmokeMcpSession(PROJECT_NAME);
    try {
      const t0 = Date.now();
      await preflight.initialize();
      serverResponding = true;
      console.log(`  ✓ Server responding (${Date.now() - t0}ms)`);
    } catch (err) {
      console.log(`  ✗ Server not responding: ${err instanceof Error ? err.message : err}`);
      console.log('    All MCP tests will be skipped');
    } finally {
      await preflight.close();
    }

    // Ensure proxymodels dir is clean (no overrides)
    try {
      await rm(join(PROXYMODELS_DIR, 'default.yaml'));
    } catch {
      // No override file
    }
  }, 120_000);

  afterAll(async () => {
    if (!available) return;
    ChatReporter.printSummary();

    // Clean up override file
    try {
      await rm(join(PROXYMODELS_DIR, 'default.yaml'));
    } catch {
      // Already clean
    }
  });

  // ── Gating ──

  it('skips if mcplocal is not running', () => {
    if (!available) {
      console.log('SKIP: mcplocal not running');
    }
    expect(true).toBe(true);
  });

  it('gated session: tools/list returns only begin_session', async () => {
    if (!serverResponding) return;

    const session = new SmokeMcpSession(PROJECT_NAME);
    const chat = new ChatReporter(session);
    chat.section('Gating: fresh session sees only begin_session');
    try {
      await chat.initialize();
      await chat.sendNotification('notifications/initialized');
      const tools = await chat.listTools();

      chat.check('Exactly 1 tool', tools.length, (v) => v === 1);
      chat.check('Tool is begin_session', tools[0]?.name ?? '', (v) => v === 'begin_session');
      chat.check('Has inputSchema', !!tools[0]?.inputSchema, (v) => v === true);

      expect(tools).toHaveLength(1);
      expect(tools[0]!.name).toBe('begin_session');
    } finally {
      await chat.close();
    }
  }, 15_000);

  it('begin_session ungates and returns full tool list', async () => {
    if (!serverResponding) return;

    const session = new SmokeMcpSession(PROJECT_NAME);
    const chat = new ChatReporter(session);
    chat.section('Ungating: begin_session reveals upstream tools');
    try {
      await chat.initialize();
      await chat.sendNotification('notifications/initialized');

      const tools = await chat.listTools();
      const bsTool = tools[0]!;
      const schema = bsTool.inputSchema as { properties?: Record<string, unknown> };
      const hasDescription = 'description' in (schema.properties ?? {});
      const hasTags = 'tags' in (schema.properties ?? {});

      const args = hasDescription
        ? { description: 'testing proxy pipeline with smoke-data' }
        : hasTags
          ? { tags: ['test', 'proxy', 'pipeline'] }
          : {};

      const result = await chat.callTool('begin_session', args, 90_000);
      chat.check('begin_session returned content', result.content.length, (v) => v > 0);

      const ungatedTools = await chat.listTools();
      chat.check('Ungated tools > 1', ungatedTools.length, (v) => v > 1);

      const awsTools = ungatedTools.filter((t) => t.name.startsWith('smoke-aws-docs/'));
      chat.check('Has smoke-aws-docs/* tools', awsTools.length, (v) => v > 0);

      expect(ungatedTools.length).toBeGreaterThan(1);
    } finally {
      await chat.close();
    }
  }, 120_000);

  // ── Prompt volume ──

  describe('Prompt volume', () => {
    it('project has prompts from fixture', async () => {
      if (!available) return;

      try {
        const output = await mcpctl(`--project ${PROJECT_NAME} get prompts -o yaml`);
        const promptCount = (output.match(/^kind: prompt$/gm) ?? []).length;

        const chat = new ChatReporter(new SmokeMcpSession(PROJECT_NAME));
        chat.section('Prompt volume');
        chat.check('Prompts loaded from fixture', promptCount, (v) => v >= 50);
        console.log(`  ℹ  ${promptCount} prompts in project`);

        expect(promptCount).toBeGreaterThan(0);
      } catch (err) {
        console.log(`  ⚠ Could not list prompts: ${err instanceof Error ? err.message : err}`);
      }
    }, 15_000);
  });

  // ── Default ProxyModel (passthrough + paginate) ──

  describe('Default proxy model', () => {
    let session: SmokeMcpSession;
    let chat: ChatReporter;
    let ungatedTools: Array<{ name: string; description?: string; inputSchema?: unknown }>;

    beforeAll(async () => {
      if (!serverResponding) return;

      try {
        await rm(join(PROXYMODELS_DIR, 'default.yaml'));
      } catch {
        // Already clean
      }

      session = new SmokeMcpSession(PROJECT_NAME);
      chat = new ChatReporter(session);
      chat.section('Default proxy model (passthrough + paginate)');
      await chat.initialize();
      await chat.sendNotification('notifications/initialized');

      const tools = await chat.listTools();
      const schema = tools[0]!.inputSchema as { properties?: Record<string, unknown> };
      const args = 'description' in (schema.properties ?? {})
        ? { description: 'test default proxy' }
        : { tags: ['test'] };
      await chat.callTool('begin_session', args, 90_000);

      ungatedTools = await chat.listTools();
    }, 120_000);

    afterAll(async () => {
      if (session) await session.close();
    });

    it('has AWS documentation tools after ungating', async () => {
      if (!serverResponding) return;

      const awsTools = ungatedTools.filter((t) => t.name.startsWith('smoke-aws-docs/'));
      chat.check('AWS docs tools available', awsTools.length, (v) => v > 0);

      if (awsTools.length > 0) {
        console.log(`    tools: ${awsTools.map((t) => t.name).join(', ')}`);
      }

      expect(awsTools.length).toBeGreaterThan(0);
    }, 10_000);

    it('can call an AWS documentation tool', async () => {
      if (!serverResponding) return;

      const searchTool = ungatedTools.find((t) => t.name === 'smoke-aws-docs/search_documentation');
      const recommendTool = ungatedTools.find((t) => t.name === 'smoke-aws-docs/recommend');
      const readTool = ungatedTools.find((t) => t.name === 'smoke-aws-docs/read_documentation');

      // Prefer search_documentation — most reliable (no URL format requirements)
      const toolToTest = searchTool ?? recommendTool ?? readTool;
      if (!toolToTest) {
        chat.skip('No testable AWS docs tool found');
        return;
      }

      let result;
      if (toolToTest.name.includes('search')) {
        result = await chat.callTool(toolToTest.name, { search_phrase: 'S3 bucket' });
      } else if (toolToTest.name.includes('recommend')) {
        result = await chat.callTool(toolToTest.name, { task: 'Store files in the cloud' });
      } else {
        result = await chat.callTool(toolToTest.name, { url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html' });
      }

      const text = result.content?.[0]?.text ?? '';
      chat.check('Tool returned content', text.length, (v) => v > 0);
      chat.check('Not an error response', !result.isError, (v) => v === true);

      expect(text.length).toBeGreaterThan(0);
    }, 60_000);

    it('large tool result gets paginated with _resultId', async () => {
      if (!serverResponding) return;

      const readTool = ungatedTools.find((t) => t.name === 'smoke-aws-docs/read_documentation');
      if (!readTool) {
        chat.skip('read_documentation not available');
        return;
      }

      const result = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
      });
      const text = result.content[0]?.text ?? '';

      chat.check('Response has content', text.length, (v) => v > 100);
      chat.check('Response is manageable size', text.length, (v) => v < 20_000);

      if (text.includes('_resultId')) {
        const match = text.match(/_resultId:\s*(\S+)/);
        chat.check('_resultId is present', !!match, (v) => v === true);
      } else {
        chat.info('Content small enough — no pagination needed');
      }
    }, 60_000);

    it('section drill-down via _resultId and _section', async () => {
      if (!serverResponding) return;

      const readTool = ungatedTools.find((t) => t.name === 'smoke-aws-docs/read_documentation');
      if (!readTool) {
        chat.skip('read_documentation not available');
        return;
      }

      const result = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
      });
      const text = result.content[0]?.text ?? '';

      const match = text.match(/_resultId:\s*(\S+)/);
      if (!match) {
        chat.info('Content not large enough for pagination — skip drill-down');
        return;
      }

      const resultId = match[1]!.replace(/[^a-zA-Z0-9-]/g, '');

      const sectionResult = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
        _resultId: resultId,
        _section: 'page-1',
      });

      expect(sectionResult.content).toBeDefined();
      const sectionText = sectionResult.content[0]?.text ?? '';
      chat.check('Section has content', sectionText.length, (v) => v > 0);
    }, 60_000);
  });

  // ── Hot-reload: switch to subindex model ──

  describe('Hot-reload: subindex model', () => {
    let session: SmokeMcpSession;
    let chat: ChatReporter;

    beforeAll(async () => {
      if (!serverResponding) return;

      // Write subindex override as 'default'
      await mkdir(PROXYMODELS_DIR, { recursive: true });
      await writeFile(
        join(PROXYMODELS_DIR, 'default.yaml'),
        [
          'kind: ProxyModel',
          'metadata:',
          '  name: default',
          'spec:',
          '  controller: gate',
          '  stages:',
          '    - type: section-split',
          '      config:',
          '        minSectionSize: 2000',
          '        maxSectionSize: 15000',
          '    - type: summarize-tree',
          '      config:',
          '        maxSummaryTokens: 200',
          '  appliesTo: [toolResult]',
          '  cacheable: true',
        ].join('\n'),
      );

      session = new SmokeMcpSession(PROJECT_NAME);
      chat = new ChatReporter(session);
      chat.section('Hot-reload: subindex proxy model');
      chat.info('Wrote subindex override to ~/.mcpctl/proxymodels/default.yaml');
      await chat.initialize();
      await chat.sendNotification('notifications/initialized');

      const tools = await chat.listTools();
      const schema = tools[0]!.inputSchema as { properties?: Record<string, unknown> };
      const args = 'description' in (schema.properties ?? {})
        ? { description: 'test subindex proxy' }
        : { tags: ['test'] };
      await chat.callTool('begin_session', args, 90_000);
    }, 120_000);

    afterAll(async () => {
      if (session) await session.close();
      try {
        await rm(join(PROXYMODELS_DIR, 'default.yaml'));
      } catch {
        // Already clean
      }
    });

    it('subindex model produces structural sections (not flat pages)', async () => {
      if (!serverResponding) return;

      const readTool = (await chat.listTools()).find((t) => t.name === 'smoke-aws-docs/read_documentation');
      if (!readTool) {
        chat.skip('read_documentation not available');
        return;
      }

      const result = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
      });
      const text = result.content[0]?.text ?? '';

      chat.check('Response has content', text.length, (v) => v > 0);
      chat.check('Response is manageable size', text.length, (v) => v < 20_000);

      if (text.includes('_resultId')) {
        const match = text.match(/_resultId:\s*(\S+)/);
        chat.check('Has _resultId for drill-down', !!match, (v) => v === true);
      }
    }, 60_000);

    it('subindex drill-down returns section content', async () => {
      if (!serverResponding) return;

      const readTool = (await chat.listTools()).find((t) => t.name === 'smoke-aws-docs/read_documentation');
      if (!readTool) {
        chat.skip('read_documentation not available');
        return;
      }

      const result = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
      });
      const text = result.content[0]?.text ?? '';

      const match = text.match(/_resultId:\s*(\S+)/);
      if (!match) {
        chat.info('Content not large enough for section-split');
        return;
      }

      const resultId = match[1]!.replace(/[^a-zA-Z0-9-]/g, '');

      const sectionResult = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
        _resultId: resultId,
        _section: 'section-0',
      });

      expect(sectionResult.content).toBeDefined();
      const sectionText = sectionResult.content[0]?.text ?? '';
      chat.check('Section content is non-empty', sectionText.length, (v) => v > 0);
    }, 60_000);
  });

  // ── Hot-reload within a session ──

  describe('Hot-reload within session', () => {
    let session: SmokeMcpSession;
    let chat: ChatReporter;

    beforeAll(async () => {
      if (!serverResponding) return;

      try {
        await rm(join(PROXYMODELS_DIR, 'default.yaml'));
      } catch {
        // Already clean
      }

      session = new SmokeMcpSession(PROJECT_NAME);
      chat = new ChatReporter(session);
      chat.section('Hot-reload within active session');
      await chat.initialize();
      await chat.sendNotification('notifications/initialized');

      const tools = await chat.listTools();
      const schema = tools[0]!.inputSchema as { properties?: Record<string, unknown> };
      const args = 'description' in (schema.properties ?? {})
        ? { description: 'test hot-reload' }
        : { tags: ['test'] };
      await chat.callTool('begin_session', args, 90_000);
    }, 120_000);

    afterAll(async () => {
      if (session) await session.close();
      try {
        await rm(join(PROXYMODELS_DIR, 'default.yaml'));
      } catch {
        // Already clean
      }
    });

    it('model changes take effect between tool calls without restart', async () => {
      if (!serverResponding) return;

      const tools = await chat.listTools();
      const readTool = tools.find((t) => t.name === 'smoke-aws-docs/read_documentation');
      if (!readTool) {
        chat.skip('read_documentation not available');
        return;
      }

      chat.info('Call 1: using default model (passthrough + paginate)');
      const result1 = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
      });
      const text1 = result1.content[0]?.text ?? '';

      // Switch proxy model mid-session
      chat.info('Swapping proxy model to tiny pages (2000 chars)...');
      await mkdir(PROXYMODELS_DIR, { recursive: true });
      await writeFile(
        join(PROXYMODELS_DIR, 'default.yaml'),
        [
          'kind: ProxyModel',
          'metadata:',
          '  name: default',
          'spec:',
          '  controller: gate',
          '  stages:',
          '    - type: passthrough',
          '    - type: paginate',
          '      config:',
          '        pageSize: 2000',
          '  appliesTo: [toolResult]',
          '  cacheable: false',
        ].join('\n'),
      );

      chat.info('Call 2: using new model (should produce different output)');
      const result2 = await chat.callTool('smoke-aws-docs/read_documentation', {
        url: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html',
      });
      const text2 = result2.content[0]?.text ?? '';

      chat.check('First call has content', text1.length, (v) => v > 0);
      chat.check('Second call has content', text2.length, (v) => v > 0);

      if (text1.includes('_resultId') || text2.includes('_resultId')) {
        chat.check('Model change affected output', text1 !== text2, (v) => v === true);
        expect(text1).not.toBe(text2);
      }
    }, 120_000);
  });

  // ── Error handling ──

  it('nonexistent project returns clear error', async () => {
    if (!serverResponding) return;

    const session = new SmokeMcpSession('nonexistent-project-xyz');
    const chat = new ChatReporter(session);
    chat.section('Error handling');
    try {
      await chat.initialize();
      expect(true).toBe(false);
    } catch (err) {
      const msg = String(err);
      chat.check('Nonexistent project gives error', true, () => /Failed to load project|HTTP|error/i.test(msg));
      expect(msg).toMatch(/Failed to load project|HTTP|error/i);
    } finally {
      await chat.close();
    }
  }, 10_000);

  it('invalid tool name returns error while gated', async () => {
    if (!serverResponding) return;

    const session = new SmokeMcpSession(PROJECT_NAME);
    const chat = new ChatReporter(session);
    try {
      await chat.initialize();
      await chat.sendNotification('notifications/initialized');

      try {
        const result = await chat.callTool('nonexistent/tool');
        chat.check('Error flag set', result.isError ?? true, (v) => v === true);
        expect(result.isError ?? true).toBe(true);
      } catch (err) {
        const msg = String(err).toLowerCase();
        chat.check('Unknown tool returns error', true, () => /error|gated|unknown|not found/.test(msg));
        expect(msg).toMatch(/error|gated|unknown|not found/);
      }
    } finally {
      await chat.close();
    }
  }, 15_000);
});
