import http from 'node:http';
import type { AddressInfo } from 'node:net';
import express from 'express';
import type { Express } from 'express';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { getArtifact, fetchProjectFile } from '../src/mcp.js';

// A minimal mock of the daemon's project file endpoints. Tests control
// the file list and per-file response via the opts object.
interface DaemonAppOpts {
  files?: Array<{ name: string }>;
  fileContent?: string;
  contentType?: string;
  contentLength?: number | null;
}

interface Harness {
  server: http.Server;
  baseUrl: string;
}

interface TextContent {
  type: string;
  text: string;
}

interface ArtifactBody {
  truncated: boolean;
  files: unknown[];
}

function firstText(content: TextContent[]): string {
  const item = content[0];
  if (item == null) throw new Error('expected MCP text content');
  return item.text;
}

function parseArtifactBody(text: string): ArtifactBody {
  return JSON.parse(text) as ArtifactBody;
}

function makeDaemonApp(opts: DaemonAppOpts = {}): Express {
  const { files = [], fileContent = 'body {}', contentType = 'text/css', contentLength = null } = opts;
  const app = express();

  app.get('/api/projects/:id', (_req, res) =>
    res.json({
      project: { id: _req.params.id, name: 'Test', metadata: { entryFile: 'index.html' } },
    }),
  );

  app.get('/api/projects/:id/files', (_req, res) => res.json({ files }));

  app.get('/api/projects/:id/raw/*splat', (_req, res) => {
    const headers: Record<string, string> = { 'content-type': contentType };
    if (contentLength != null) headers['content-length'] = String(contentLength);
    res.set(headers).send(fileContent);
  });

  return app;
}

function startServer(app: Express): Promise<Harness> {
  return new Promise((resolve) => {
    const tmp = http.createServer();
    tmp.listen(0, '127.0.0.1', () => {
      const { port } = tmp.address() as AddressInfo;
      tmp.close(() => {
        const server = app.listen(port, '127.0.0.1', () =>
          resolve({ server, baseUrl: `http://127.0.0.1:${port}` }),
        );
      });
    });
  });
}

const PROJECT_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';

describe('getArtifact file-count cap (MAX_FILES = 200)', () => {
  let server: http.Server;
  let baseUrl: string;

  const fileList = Array.from({ length: 250 }, (_, i) => ({ name: `file${i}.css` }));

  beforeAll(async () => {
    const r = await startServer(makeDaemonApp({ files: fileList, fileContent: 'a {}', contentType: 'text/css' }));
    server = r.server;
    baseUrl = r.baseUrl;
  });

  afterAll(() => new Promise((resolve) => server.close(resolve)));

  it('caps at 200 files and sets truncated: true when the project has 250 files', async () => {
    const result = await getArtifact(baseUrl, PROJECT_ID, 'index.html', 'all', 10_000_000);
    const body = parseArtifactBody(firstText(result.content));
    expect(body.truncated).toBe(true);
    expect(body.files.length).toBe(200);
  });
});

describe('getArtifact maxBytes cap', () => {
  let server: http.Server;
  let baseUrl: string;

  // 10 files, each 200 bytes. With maxBytes=400 the third loop iteration
  // finds totalTextBytes >= maxBytes and sets truncated: true.
  const fileList = Array.from({ length: 10 }, (_, i) => ({ name: `file${i}.css` }));
  const fileContent = 'a'.repeat(200);

  beforeAll(async () => {
    const r = await startServer(makeDaemonApp({ files: fileList, fileContent, contentType: 'text/css' }));
    server = r.server;
    baseUrl = r.baseUrl;
  });

  afterAll(() => new Promise((resolve) => server.close(resolve)));

  it('stops fetching and sets truncated: true when byte cap is reached', async () => {
    const result = await getArtifact(baseUrl, PROJECT_ID, 'index.html', 'all', 400);
    const body = parseArtifactBody(firstText(result.content));
    expect(body.truncated).toBe(true);
    expect(body.files.length).toBeLessThan(10);
  });
});

describe('fetchProjectFile per-file size pre-check', () => {
  let server: http.Server;
  let baseUrl: string;

  beforeAll(async () => {
    const r = await startServer(
      makeDaemonApp({ fileContent: 'x'.repeat(10_000), contentType: 'text/css', contentLength: 10_000 }),
    );
    server = r.server;
    baseUrl = r.baseUrl;
  });

  afterAll(() => new Promise((resolve) => server.close(resolve)));

  it('throws when content-length exceeds remainingBytes without reading the body', async () => {
    await expect(fetchProjectFile(baseUrl, PROJECT_ID, 'styles.css', 5_000)).rejects.toThrow(
      /exceeds remaining budget/,
    );
  });

  it('succeeds and returns content when remainingBytes is sufficient', async () => {
    const file = await fetchProjectFile(baseUrl, PROJECT_ID, 'styles.css', 20_000);
    expect(file.binary).toBe(false);
    expect(file.content?.length).toBe(10_000);
  });
});

describe('getArtifact truncated: true when per-file content-length pre-check fires (include=all)', () => {
  let server: http.Server;
  let baseUrl: string;

  // 5 files, each 250 bytes with explicit content-length.
  // maxBytes=400: file0 (remaining=400, size=250) fetches fine.
  // file1+ (remaining=150, size=250 > 150) hit the BudgetExceededError path.
  // totalTextBytes never reaches maxBytes, so only the pre-check path sets truncated.
  const fileList = Array.from({ length: 5 }, (_, i) => ({ name: `file${i}.css` }));
  const fileContent = 'a'.repeat(250);

  beforeAll(async () => {
    const r = await startServer(
      makeDaemonApp({ files: fileList, fileContent, contentType: 'text/css', contentLength: 250 }),
    );
    server = r.server;
    baseUrl = r.baseUrl;
  });

  afterAll(() => new Promise((resolve) => server.close(resolve)));

  it('sets truncated: true even when totalTextBytes never reaches maxBytes', async () => {
    const result = await getArtifact(baseUrl, PROJECT_ID, 'index.html', 'all', 400);
    const body = parseArtifactBody(firstText(result.content));
    expect(body.truncated).toBe(true);
    expect(body.files.length).toBe(1);
  });
});
