import { execFile } from 'node:child_process';
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { describe, expect, it } from 'vitest';

const CLI_ENTRY = fileURLToPath(new URL('../dist/cli.js', import.meta.url));
const testRequire = createRequire(import.meta.url);
const MCP_SERVER_MODULE = pathToFileURL(testRequire.resolve('@modelcontextprotocol/sdk/server/mcp.js')).href;
const STDIO_SERVER_MODULE = pathToFileURL(testRequire.resolve('@modelcontextprotocol/sdk/server/stdio.js')).href;
const ZOD_MODULE = pathToFileURL(testRequire.resolve('zod')).href;
const describeDaemon = process.platform === 'win32' ? describe.skip : describe;
const PNPM_COMMAND = process.platform === 'win32' ? 'cmd.exe' : 'pnpm';
const PNPM_ARGS_PREFIX = process.platform === 'win32' ? ['/d', '/s', '/c', 'pnpm'] : [];

function pnpmArgs(args: string[]): string[] {
  return [...PNPM_ARGS_PREFIX, ...args];
}

async function readFileWithRetries(filePath: string, retries = 20, delayMs = 100): Promise<string> {
  let lastError: unknown;
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      return await fs.readFile(filePath, 'utf8');
    } catch (error) {
      lastError = error;
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
        throw error;
      }
    }
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  throw lastError ?? new Error(`Failed to read ${filePath}`);
}

async function ensureDistBuilt(): Promise<void> {
  try {
    await fs.access(CLI_ENTRY);
  } catch {
    await new Promise<void>((resolve, reject) => {
      execFile(PNPM_COMMAND, pnpmArgs(['build']), { cwd: process.cwd(), env: process.env }, (error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      });
    });
  }
}

async function runCli(
  args: string[],
  configPath: string,
  envOverrides: Record<string, string> = {}
): Promise<{ stdout: string; stderr: string }> {
  return await new Promise((resolve, reject) => {
    execFile(
      process.execPath,
      [CLI_ENTRY, '--config', configPath, ...args],
      {
        env: { ...process.env, MCPORTER_NO_FORCE_EXIT: '1', ...envOverrides },
      },
      (error, stdout, stderr) => {
        if (error) {
          const wrapped = new Error(`${error.message}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`);
          reject(wrapped);
          return;
        }
        resolve({ stdout, stderr });
      }
    );
  });
}

function parseCliJson(output: string): { instanceId: string; count: number } {
  const trimmed = output.trim();
  const start = trimmed.indexOf('{');
  const end = trimmed.lastIndexOf('}');
  if (start === -1 || end === -1 || end < start) {
    throw new Error(`Unable to locate JSON payload in CLI output:\n${output}`);
  }
  return JSON.parse(trimmed.slice(start, end + 1));
}

describeDaemon('daemon keep-alive integration', () => {
  it('reuses stdio servers across mcporter invocations', async () => {
    await ensureDistBuilt();
    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-daemon-e2e-'));
    const scriptPath = path.join(tempDir, 'daemon-server.mjs');
    const configPath = path.join(tempDir, 'mcporter.daemon.json');

    const stdioServerSource = `import { randomUUID } from 'node:crypto';
import { McpServer } from '${MCP_SERVER_MODULE}';
import { StdioServerTransport } from '${STDIO_SERVER_MODULE}';
import { z } from '${ZOD_MODULE}';

const instanceId = randomUUID();
let counter = 0;

const server = new McpServer({ name: 'daemon-e2e', version: '1.0.0' });
server.registerTool('next_value', {
  title: 'Next value',
  description: 'Returns an incrementing counter along with the server instance id.',
  inputSchema: {},
  outputSchema: {
    instanceId: z.string(),
    count: z.number(),
  },
}, async () => {
  counter += 1;
  return {
    content: [{ type: 'text', text: JSON.stringify({ instanceId, count: counter }) }],
    structuredContent: { instanceId, count: counter },
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);
await new Promise((resolve) => {
  transport.onclose = resolve;
});
`;

    await fs.writeFile(scriptPath, stdioServerSource, 'utf8');
    await fs.writeFile(
      configPath,
      JSON.stringify(
        {
          mcpServers: {
            'daemon-e2e': {
              description: 'E2E daemon test server',
              command: 'node',
              args: [scriptPath],
              lifecycle: 'keep-alive',
            },
          },
        },
        null,
        2
      ),
      'utf8'
    );

    const logPath = path.join(tempDir, 'daemon.log');
    const cliEnv = {
      MCPORTER_DAEMON_LOG: '1',
      MCPORTER_DAEMON_LOG_PATH: logPath,
      MCPORTER_DAEMON_LOG_SERVERS: 'daemon-e2e',
    };
    const cli = (args: string[]) => runCli(args, configPath, cliEnv);

    try {
      await cli(['daemon', 'stop']);

      const first = await cli(['call', 'daemon-e2e.next_value', '--output', 'json']);
      const firstResult = parseCliJson(first.stdout);
      expect(firstResult.count).toBe(1);

      const second = await cli(['call', 'daemon-e2e.next_value', '--output', 'json']);
      const secondResult = parseCliJson(second.stdout);
      expect(secondResult.count).toBe(2);
      expect(secondResult.instanceId).toBe(firstResult.instanceId);

      const logContents = await readFileWithRetries(logPath);
      expect(logContents).toContain('callTool start server=daemon-e2e tool=next_value');
      expect(logContents).toContain('callTool success server=daemon-e2e tool=next_value');
    } finally {
      await cli(['daemon', 'stop']).catch(() => {});
      await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
    }
  }, 40_000);
});
