// @vitest-environment node
/* eslint-disable security/detect-non-literal-fs-filename, sonarjs/publicly-writable-directories */
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import http from 'node:http';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';

import { AgentTeamsMcpHttpServer } from '@main/services/team/AgentTeamsMcpHttpServer';
import { OpenCodeBridgeCommandClient } from '@main/services/team/opencode/bridge/OpenCodeBridgeCommandClient';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const FAKE_MCP_HTTP_SERVER_SOURCE = String.raw`
const fs = require('node:fs');
const http = require('node:http');

function readArg(name) {
  const index = process.argv.indexOf(name);
  return index >= 0 ? process.argv[index + 1] : null;
}

const host = readArg('--host') || '127.0.0.1';
const endpoint = readArg('--endpoint') || '/mcp';
const port = Number(readArg('--port'));
const controlFile = process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE;
const healthIdentity =
  process.env.AGENT_TEAMS_MCP_HTTP_IDENTITY_SERVICE === 'agent-teams-mcp-http' &&
  process.env.AGENT_TEAMS_MCP_HTTP_CLAUDE_DIR_HASH &&
  process.env.AGENT_TEAMS_MCP_HTTP_LAUNCH_SPEC_HASH &&
  process.env.AGENT_TEAMS_MCP_HTTP_OWNER_INSTANCE_ID
    ? {
        schemaVersion: 1,
        service: 'agent-teams-mcp-http',
        transport: 'httpStream',
        host,
        port,
        endpoint,
        claudeDirHash: process.env.AGENT_TEAMS_MCP_HTTP_CLAUDE_DIR_HASH,
        launchSpecHash: process.env.AGENT_TEAMS_MCP_HTTP_LAUNCH_SPEC_HASH,
        ownerInstanceId: process.env.AGENT_TEAMS_MCP_HTTP_OWNER_INSTANCE_ID,
      }
    : null;

function readControl() {
  if (!controlFile) {
    return 'healthy';
  }
  try {
    return fs.readFileSync(controlFile, 'utf8').trim() || 'healthy';
  } catch {
    return 'healthy';
  }
}

function isUnhealthy() {
  const control = readControl();
  if (control === 'unhealthy-once:' + port) {
    try {
      fs.writeFileSync(controlFile, 'healthy');
    } catch {}
    return true;
  }
  if (control === 'unhealthy-pid:' + process.pid) {
    return true;
  }
  return control === 'unhealthy-all' || control === 'unhealthy-port:' + port;
}

const server = http.createServer((request, response) => {
  if (request.url === '/health') {
    if (isUnhealthy()) {
      response.writeHead(503, { 'content-type': 'text/plain' });
      response.end('unhealthy');
      return;
    }
    response.writeHead(200, { 'content-type': 'text/plain' });
    response.end(healthIdentity ? JSON.stringify(healthIdentity) : 'ok');
    return;
  }

  if (request.url === endpoint) {
    response.writeHead(200, { 'content-type': 'application/json' });
    response.end('{"jsonrpc":"2.0","result":{}}');
    return;
  }

  response.writeHead(404, { 'content-type': 'text/plain' });
  response.end('not found');
});

server.listen(port, host);

function shutdown() {
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(0), 500).unref();
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
`;

const FAKE_OPENCODE_BRIDGE_BINARY_SOURCE = String.raw`#!/usr/bin/env node
const fs = require('node:fs');
const http = require('node:http');

function readArg(name) {
  const index = process.argv.indexOf(name);
  return index >= 0 ? process.argv[index + 1] : null;
}

function readHealthStatus(url) {
  return new Promise((resolve) => {
    if (!url) {
      resolve(null);
      return;
    }
    const target = new URL(url);
    target.pathname = '/health';
    target.search = '';
    target.hash = '';
    const request = http.get(
      {
        host: target.hostname,
        port: Number(target.port),
        path: target.pathname,
        timeout: 750,
      },
      (response) => {
        response.resume();
        resolve(response.statusCode || null);
      }
    );
    request.once('timeout', () => {
      request.destroy();
      resolve(null);
    });
    request.once('error', () => resolve(null));
  });
}

async function main() {
  const inputPath = readArg('--input');
  if (!inputPath) {
    console.error('missing --input');
    process.exit(64);
  }

  const envelope = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
  const mcpUrl = process.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL || null;
  const healthStatus = await readHealthStatus(mcpUrl);
  if (healthStatus !== 200) {
    console.error(
      JSON.stringify({
        kind: 'mcp_unreachable',
        mcpUrl,
        healthStatus,
      })
    );
    process.exit(7);
  }

  process.stdout.write(
    JSON.stringify({
      ok: true,
      schemaVersion: envelope.schemaVersion,
      requestId: envelope.requestId,
      command: envelope.command,
      completedAt: new Date().toISOString(),
      durationMs: 1,
      runtime: {
        providerId: 'opencode',
        binaryPath: process.argv[1],
        binaryFingerprint: 'fake-runtime',
        version: 'fake-opencode-bridge-e2e',
        capabilitySnapshotId: 'fake-capabilities',
      },
      diagnostics: [],
      data: {
        runId: envelope.body && envelope.body.runId ? envelope.body.runId : null,
        observedMcpUrl: mcpUrl,
        healthStatus,
      },
    }) + '\n'
  );
}

main().catch((error) => {
  console.error(error && error.stack ? error.stack : String(error));
  process.exit(1);
});
`;

const describePosix = process.platform === 'win32' ? describe.skip : describe;

async function allocateLoopbackPort(excluded: Set<number> = new Set<number>()): Promise<number> {
  while (true) {
    const port = await new Promise<number>((resolve, reject) => {
      const server = net.createServer();
      server.once('error', reject);
      server.listen(0, '127.0.0.1', () => {
        const address = server.address();
        if (!address || typeof address === 'string') {
          server.close(() => reject(new Error('Failed to allocate test port')));
          return;
        }
        server.close(() => resolve(address.port));
      });
    });
    if (!excluded.has(port)) {
      excluded.add(port);
      return port;
    }
  }
}

async function readHealthStatus(url: string): Promise<number | null> {
  const target = new URL(url);
  target.pathname = '/health';
  target.search = '';
  target.hash = '';

  return new Promise((resolve) => {
    const request = http.get(
      {
        host: target.hostname,
        port: Number(target.port),
        path: target.pathname,
        timeout: 500,
      },
      (response) => {
        response.resume();
        resolve(response.statusCode ?? null);
      }
    );
    request.once('timeout', () => {
      request.destroy();
      resolve(null);
    });
    request.once('error', () => resolve(null));
  });
}

async function waitForHealthDown(url: string): Promise<void> {
  const startedAt = Date.now();
  while (Date.now() - startedAt < 5_000) {
    if ((await readHealthStatus(url)) !== 200) {
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
  throw new Error(`Expected ${url} health endpoint to go down`);
}

async function writeFakeMcpHttpServer(tempDir: string): Promise<string> {
  const scriptDir = path.join(tempDir, 'fake-mcp');
  const scriptPath = path.join(scriptDir, 'server.cjs');
  await mkdir(scriptDir, { recursive: true });
  await writeFile(scriptPath, FAKE_MCP_HTTP_SERVER_SOURCE, 'utf8');
  return scriptPath;
}

async function writeFakeOpenCodeBridgeBinary(tempDir: string): Promise<string> {
  const scriptDir = path.join(tempDir, 'fake-runtime');
  const scriptPath = path.join(scriptDir, 'claude-multimodel-fake');
  await mkdir(scriptDir, { recursive: true });
  await writeFile(scriptPath, FAKE_OPENCODE_BRIDGE_BINARY_SOURCE, 'utf8');
  await chmod(scriptPath, 0o755);
  return scriptPath;
}

describePosix('AgentTeamsMcpHttpServer integration', () => {
  let tempDir: string | null = null;
  let originalControlFileEnv: string | undefined;
  let originalMcpHttpPortEnv: string | undefined;
  const servers: AgentTeamsMcpHttpServer[] = [];

  beforeEach(async () => {
    tempDir = await mkdtemp(path.join(os.tmpdir(), 'agent-teams-mcp-http-integration-'));
    originalControlFileEnv = process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE;
    originalMcpHttpPortEnv = process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT;
  });

  afterEach(async () => {
    await Promise.all(servers.splice(0).map((server) => server.stop()));
    vi.mocked(console.warn).mockClear();
    if (originalControlFileEnv === undefined) {
      delete process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE;
    } else {
      process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE = originalControlFileEnv;
    }
    if (originalMcpHttpPortEnv === undefined) {
      delete process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT;
    } else {
      process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT = originalMcpHttpPortEnv;
    }
    if (tempDir) {
      await rm(tempDir, { recursive: true, force: true });
      tempDir = null;
    }
  });

  function createControlledServer(input: {
    scriptPath: string;
    controlFile: string;
    allocatePort?: () => Promise<number>;
    statePath?: string;
  }): AgentTeamsMcpHttpServer {
    const server = new AgentTeamsMcpHttpServer({
      statePath: input.statePath ?? path.join(tempDir!, `mcp-http-state-${servers.length}.json`),
      disableOrphanCleanup: true,
      resolveLaunchSpec: () =>
        Promise.resolve({
          command: process.execPath,
          args: [input.scriptPath],
        }),
      allocatePort: input.allocatePort,
    });
    servers.push(server);

    process.env.AGENT_TEAMS_MCP_TEST_CONTROL_FILE = input.controlFile;
    return server;
  }

  it('starts the actual Agent Teams MCP HTTP server and proves its health endpoint', async () => {
    const server = new AgentTeamsMcpHttpServer({
      statePath: path.join(tempDir!, 'actual-mcp-http-state.json'),
      disableOrphanCleanup: true,
    });
    servers.push(server);

    const handle = await server.ensureStarted();

    expect(handle.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/mcp$/);
    expect(handle.pid).toEqual(expect.any(Number));
    expect(await readHealthStatus(handle.url)).toBe(200);
  });

  it('reuses a healthy cached bridge URL after a real loopback health recheck', async () => {
    const scriptPath = await writeFakeMcpHttpServer(tempDir!);
    const controlFile = path.join(tempDir!, 'health-control.txt');
    await writeFile(controlFile, 'healthy', 'utf8');
    const server = createControlledServer({ scriptPath, controlFile });

    const first = await server.ensureStarted();
    const warnCountAfterFirstStart = vi.mocked(console.warn).mock.calls.length;
    const second = await server.ensureStarted();

    expect(second).toEqual(first);
    expect(await readHealthStatus(first.url)).toBe(200);
    expect(vi.mocked(console.warn).mock.calls.slice(warnCountAfterFirstStart)).toEqual([]);
  });

  it('adopts a healthy MCP HTTP child from persistent state across server managers', async () => {
    const scriptPath = await writeFakeMcpHttpServer(tempDir!);
    const controlFile = path.join(tempDir!, 'health-control.txt');
    const statePath = path.join(tempDir!, 'shared-mcp-http-state.json');
    await writeFile(controlFile, 'healthy', 'utf8');
    const firstServer = createControlledServer({ scriptPath, controlFile, statePath });

    const first = await firstServer.ensureStarted();
    const secondServer = createControlledServer({ scriptPath, controlFile, statePath });
    const second = await secondServer.ensureStarted();

    expect(second.port).toBe(first.port);
    expect(second.pid).toBe(first.pid);
    expect(second.diagnostics).toContain(`opencode_app_mcp_adopted_state_server:${first.port}`);
    expect(await readHealthStatus(second.url)).toBe(200);
  });

  it('falls back from an unknown healthy stable-port server and leaves it alive', async () => {
    const scriptPath = await writeFakeMcpHttpServer(tempDir!);
    const controlFile = path.join(tempDir!, 'health-control.txt');
    await writeFile(controlFile, 'healthy', 'utf8');

    let unknownServer: http.Server | null = null;
    let stablePort = 0;
    let fallbackPort = 0;
    for (let candidate = 43_100; candidate < 65_534; candidate += 1) {
      const stableCandidate = http.createServer((request, response) => {
        if (request.url === '/health') {
          response.writeHead(200, { 'content-type': 'text/plain' });
          response.end('unknown-ok');
          return;
        }
        response.writeHead(404);
        response.end('not found');
      });
      const fallbackProbe = net.createServer();
      try {
        await new Promise<void>((resolve, reject) => {
          stableCandidate.once('error', reject);
          stableCandidate.listen(candidate, '127.0.0.1', () => resolve());
        });
        await new Promise<void>((resolve, reject) => {
          fallbackProbe.once('error', reject);
          fallbackProbe.listen(candidate + 1, '127.0.0.1', () => resolve());
        });
        await new Promise<void>((resolve) => fallbackProbe.close(() => resolve()));
        unknownServer = stableCandidate;
        stablePort = candidate;
        fallbackPort = candidate + 1;
        break;
      } catch {
        await new Promise<void>((resolve) => fallbackProbe.close(() => resolve())).catch(
          () => undefined
        );
        await new Promise<void>((resolve) => stableCandidate.close(() => resolve())).catch(
          () => undefined
        );
      }
    }
    if (!unknownServer) {
      throw new Error('Failed to reserve contiguous ports for unknown stable-port test');
    }

    process.env.CLAUDE_TEAM_OPENCODE_MCP_HTTP_PORT = String(stablePort);
    const server = createControlledServer({ scriptPath, controlFile });

    try {
      const handle = await server.ensureStarted();

      expect(handle.port).toBe(fallbackPort);
      expect(handle.diagnostics).toContain(`opencode_app_mcp_port_occupied_unknown:${stablePort}`);
      expect(handle.diagnostics).toContain(
        `opencode_app_mcp_preferred_port_unavailable:${stablePort}`
      );
      expect(await readHealthStatus(`http://127.0.0.1:${stablePort}/mcp`)).toBe(200);
      expect(await readHealthStatus(handle.url)).toBe(200);
    } finally {
      await new Promise<void>((resolve) => unknownServer?.close(() => resolve()));
      vi.mocked(console.warn).mockClear();
    }
  });

  it('restarts a stale but still-running MCP HTTP child when cached URL health turns unhealthy', async () => {
    const scriptPath = await writeFakeMcpHttpServer(tempDir!);
    const controlFile = path.join(tempDir!, 'health-control.txt');
    const usedPorts = new Set<number>();
    await writeFile(controlFile, 'healthy', 'utf8');
    const server = createControlledServer({
      scriptPath,
      controlFile,
      allocatePort: () => allocateLoopbackPort(usedPorts),
    });

    const first = await server.ensureStarted();
    expect(first.pid).toEqual(expect.any(Number));
    await writeFile(controlFile, `unhealthy-pid:${first.pid}`, 'utf8');

    const second = await server.ensureStarted();

    expect(second.port).toBe(first.port);
    expect(second.url).toBe(first.url);
    expect(second.pid).not.toBe(first.pid);
    expect(await readHealthStatus(second.url)).toBe(200);
    expect(vi.mocked(console.warn).mock.calls[0]?.join(' ')).toContain('failed health reuse check');
    vi.mocked(console.warn).mockClear();
  });

  it('recovers when the cached MCP HTTP child dies and the old URL refuses connections', async () => {
    const scriptPath = await writeFakeMcpHttpServer(tempDir!);
    const controlFile = path.join(tempDir!, 'health-control.txt');
    const usedPorts = new Set<number>();
    await writeFile(controlFile, 'healthy', 'utf8');
    const server = createControlledServer({
      scriptPath,
      controlFile,
      allocatePort: () => allocateLoopbackPort(usedPorts),
    });

    const first = await server.ensureStarted();
    expect(first.pid).toEqual(expect.any(Number));
    process.kill(first.pid!, 'SIGTERM');
    await waitForHealthDown(first.url);

    const second = await server.ensureStarted();

    expect(second.port).not.toBe(first.port);
    expect(second.pid).not.toBe(first.pid);
    expect(await readHealthStatus(second.url)).toBe(200);
  });

  it('passes a refreshed healthy MCP URL into a real bridge child process after cached health goes stale', async () => {
    const scriptPath = await writeFakeMcpHttpServer(tempDir!);
    const bridgeBinaryPath = await writeFakeOpenCodeBridgeBinary(tempDir!);
    const controlFile = path.join(tempDir!, 'health-control.txt');
    const usedPorts = new Set<number>();
    await writeFile(controlFile, 'healthy', 'utf8');
    const server = createControlledServer({
      scriptPath,
      controlFile,
      allocatePort: () => allocateLoopbackPort(usedPorts),
    });
    const bridgeEnv: NodeJS.ProcessEnv = {
      PATH: process.env.PATH,
    };
    let requestIdCounter = 0;
    const client = new OpenCodeBridgeCommandClient({
      binaryPath: bridgeBinaryPath,
      tempDirectory: path.join(tempDir!, 'bridge-input'),
      env: bridgeEnv,
      envProvider: async () => {
        const mcpHttpServer = await server.ensureStarted();
        bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL = mcpHttpServer.url;
        return {
          ...bridgeEnv,
          CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: mcpHttpServer.url,
        };
      },
      requestIdFactory: () => {
        requestIdCounter += 1;
        return `req-refresh-${requestIdCounter}`;
      },
    });

    const firstResult = await client.execute<{ runId: string }, { observedMcpUrl: string }>(
      'opencode.launchTeam',
      { runId: 'run-1' },
      {
        cwd: tempDir!,
        timeoutMs: 5_000,
      }
    );

    expect(firstResult.ok).toBe(true);
    if (!firstResult.ok) {
      throw new Error(firstResult.error.message);
    }
    const firstHandle = server.getCurrentHandle();
    expect(firstHandle?.pid).toEqual(expect.any(Number));
    await writeFile(
      controlFile,
      `unhealthy-pid:${firstHandle?.pid}`,
      'utf8'
    );

    const secondResult = await client.execute<{ runId: string }, { observedMcpUrl: string }>(
      'opencode.launchTeam',
      { runId: 'run-2' },
      {
        cwd: tempDir!,
        timeoutMs: 5_000,
      }
    );

    expect(secondResult.ok).toBe(true);
    if (!secondResult.ok) {
      throw new Error(secondResult.error.message);
    }
    expect(secondResult.data.observedMcpUrl).toBe(firstResult.data.observedMcpUrl);
    expect(await readHealthStatus(secondResult.data.observedMcpUrl)).toBe(200);
  });

  it('fails closed when a bridge child receives an unreachable MCP URL without env refresh', async () => {
    const scriptPath = await writeFakeMcpHttpServer(tempDir!);
    const bridgeBinaryPath = await writeFakeOpenCodeBridgeBinary(tempDir!);
    const controlFile = path.join(tempDir!, 'health-control.txt');
    await writeFile(controlFile, 'healthy', 'utf8');
    const server = createControlledServer({ scriptPath, controlFile });

    const first = await server.ensureStarted();
    await server.stop();
    await waitForHealthDown(first.url);

    const client = new OpenCodeBridgeCommandClient({
      binaryPath: bridgeBinaryPath,
      tempDirectory: path.join(tempDir!, 'bridge-input-stale'),
      env: {
        PATH: process.env.PATH,
        CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: first.url,
      },
      requestIdFactory: () => 'req-stale-mcp',
    });

    const result = await client.execute<{ runId: string }, { observedMcpUrl: string }>(
      'opencode.launchTeam',
      { runId: 'run-stale' },
      {
        cwd: tempDir!,
        timeoutMs: 5_000,
      }
    );

    expect(result.ok).toBe(false);
    if (result.ok) {
      throw new Error('Expected stale MCP URL to fail');
    }
    expect(result.error.kind).toBe('provider_error');
    expect(result.error.details?.stderr).toContain('mcp_unreachable');
    expect(result.error.details?.stderr).toContain(first.url);
  });
});
