import { spawn, type ChildProcess } from 'node:child_process';
import { existsSync } from 'node:fs';
import { promises as fs } from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
import { setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';

describe('Team runtime memory safe e2e', () => {
  let tempDir: string;
  let child: ChildProcess | null;

  beforeEach(async () => {
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-runtime-memory-e2e-'));
    await fs.mkdir(path.join(tempDir, '.claude'), { recursive: true });
    setClaudeBasePathOverride(path.join(tempDir, '.claude'));
    child = null;
  });

  afterEach(async () => {
    if (child?.pid) {
      child.kill('SIGTERM');
      await waitForExit(child, 2_000).catch(() => {
        if (child?.pid) child.kill('SIGKILL');
      });
    }
    setClaudeBasePathOverride(null);
    await fs.rm(tempDir, { recursive: true, force: true });
  });

  const nativeProcessTableIt =
    process.platform === 'win32' || process.env.TEAM_RUNTIME_MEMORY_NATIVE_SMOKE !== '1'
      ? it.skip
      : it;

  nativeProcessTableIt('reports RSS for a bootstrap-confirmed Anthropic teammate discovered from the real process table', async () => {
    const teamName = `anthropic-rss-${process.pid}`;
    const memberName = 'alice';
    const agentId = `${memberName}@${teamName}`;
    const projectPath = path.join(tempDir, 'project');
    const runtimeScriptPath = path.join(tempDir, 'anthropic-runtime-fixture.mjs');
    await fs.mkdir(projectPath, { recursive: true });
    await fs.writeFile(
      runtimeScriptPath,
      [
        'const keepAlive = setInterval(() => {}, 1000);',
        "process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); });",
      ].join('\n'),
      'utf8'
    );
    await writeTeamFixture({
      tempDir,
      teamName,
      projectPath,
      memberName,
      agentId,
    });

    child = spawn(
      process.execPath,
      [
        runtimeScriptPath,
        '--agent-id',
        agentId,
        '--agent-name',
        memberName,
        '--team-name',
        teamName,
        '--model',
        'claude-sonnet-4-6',
      ],
      {
        cwd: projectPath,
        stdio: 'ignore',
      }
    );
    expect(child.pid).toEqual(expect.any(Number));
    await waitForProcessCommand(child.pid!, agentId, teamName);

    const snapshot = await new TeamProvisioningService().getTeamAgentRuntimeSnapshot(teamName);

    expect(snapshot.members[memberName]).toMatchObject({
      alive: true,
      providerId: 'anthropic',
      pid: child.pid,
      pidSource: 'agent_process_table',
      livenessKind: 'runtime_process',
      runtimeModel: 'claude-sonnet-4-6',
      historicalBootstrapConfirmed: true,
    });
    expect(snapshot.members[memberName]?.rssBytes).toEqual(expect.any(Number));
    expect(snapshot.members[memberName]?.rssBytes).toBeGreaterThan(0);
  });

  const cliSmokeIt =
    process.env.ANTHROPIC_RUNTIME_MEMORY_CLI_SMOKE === '1' &&
    process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() &&
    existsSync(process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH.trim())
      ? it
      : it.skip;

  cliSmokeIt('reports RSS for a real Anthropic teammate CLI process', async () => {
    const teamName = `anthropic-cli-rss-${process.pid}`;
    const memberName = 'alice';
    const agentId = `${memberName}@${teamName}`;
    const projectPath = path.join(tempDir, 'project');
    const cliPath = process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH!.trim();
    await fs.mkdir(projectPath, { recursive: true });
    await writeTeamFixture({
      tempDir,
      teamName,
      projectPath,
      memberName,
      agentId,
    });

    let stderrTail = '';
    child = spawn(
      cliPath,
      [
        '--agent-id',
        agentId,
        '--agent-name',
        memberName,
        '--team-name',
        teamName,
        '--model',
        'claude-sonnet-4-6',
      ],
      {
        cwd: projectPath,
        env: {
          ...process.env,
          CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
          NO_UPDATE_NOTIFIER: '1',
        },
        stdio: ['pipe', 'ignore', 'pipe'],
      }
    );
    child.stderr?.on('data', (chunk) => {
      stderrTail = `${stderrTail}${String(chunk)}`.slice(-4_000);
    });
    expect(child.pid).toEqual(expect.any(Number));
    await waitForProcessCommand(child.pid!, agentId, teamName, () => stderrTail);

    const snapshot = await new TeamProvisioningService().getTeamAgentRuntimeSnapshot(teamName);

    expect(snapshot.members[memberName]).toMatchObject({
      alive: true,
      providerId: 'anthropic',
      pid: child.pid,
      pidSource: 'agent_process_table',
      livenessKind: 'runtime_process',
      runtimeModel: 'claude-sonnet-4-6',
      historicalBootstrapConfirmed: true,
    });
    expect(snapshot.members[memberName]?.rssBytes).toEqual(expect.any(Number));
    expect(snapshot.members[memberName]?.rssBytes).toBeGreaterThan(0);
  });
});

async function writeTeamFixture(params: {
  tempDir: string;
  teamName: string;
  projectPath: string;
  memberName: string;
  agentId: string;
}): Promise<void> {
  const teamDir = path.join(params.tempDir, '.claude', 'teams', params.teamName);
  await fs.mkdir(teamDir, { recursive: true });
  await fs.writeFile(
    path.join(teamDir, 'config.json'),
    `${JSON.stringify(
      {
        name: params.teamName,
        projectPath: params.projectPath,
        leadSessionId: 'lead-session',
        members: [
          {
            name: 'team-lead',
            agentType: 'team-lead',
            role: 'Lead',
            providerId: 'anthropic',
          },
          {
            name: params.memberName,
            role: 'Developer',
            providerId: 'anthropic',
            model: 'claude-sonnet-4-6',
            agentId: params.agentId,
            backendType: 'tmux',
          },
        ],
      },
      null,
      2
    )}\n`,
    'utf8'
  );
  await fs.writeFile(
    path.join(teamDir, 'launch-state.json'),
    `${JSON.stringify(
      {
        version: 2,
        teamName: params.teamName,
        updatedAt: '2026-04-24T12:00:00.000Z',
        leadSessionId: 'lead-session',
        launchPhase: 'active',
        expectedMembers: [params.memberName],
        members: {
          [params.memberName]: {
            name: params.memberName,
            providerId: 'anthropic',
            model: 'claude-sonnet-4-6',
            launchState: 'confirmed_alive',
            agentToolAccepted: true,
            runtimeAlive: true,
            bootstrapConfirmed: true,
            hardFailure: false,
            lastHeartbeatAt: '2026-04-24T12:00:00.000Z',
            lastEvaluatedAt: '2026-04-24T12:00:00.000Z',
          },
        },
        summary: {
          confirmedCount: 1,
          pendingCount: 0,
          failedCount: 0,
          runtimeAlivePendingCount: 0,
        },
        teamLaunchState: 'clean_success',
      },
      null,
      2
    )}\n`,
    'utf8'
  );
}

async function waitForProcessCommand(
  pid: number,
  agentId: string,
  teamName: string,
  getDebugTail: () => string = () => ''
): Promise<void> {
  const deadline = Date.now() + 5_000;
  while (Date.now() < deadline) {
    const output = await readProcessCommand(pid).catch(() => '');
    if (output.includes(agentId) && output.includes(teamName)) {
      return;
    }
    await sleep(100);
  }
  const debugTail = getDebugTail().trim();
  throw new Error(
    `Process ${pid} did not appear in ps with expected team identity${
      debugTail ? `\nCLI stderr tail:\n${debugTail}` : ''
    }`
  );
}

function readProcessCommand(pid: number): Promise<string> {
  return new Promise((resolve, reject) => {
    const ps = spawn('ps', ['-p', String(pid), '-o', 'command='], {
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    let stdout = '';
    let stderr = '';
    ps.stdout?.on('data', (chunk) => {
      stdout += String(chunk);
    });
    ps.stderr?.on('data', (chunk) => {
      stderr += String(chunk);
    });
    ps.on('error', reject);
    ps.on('close', (code) => {
      if (code === 0) {
        resolve(stdout.trim());
      } else {
        reject(new Error(stderr.trim() || `ps exited with ${code}`));
      }
    });
  });
}

function waitForExit(child: ChildProcess, timeoutMs: number): Promise<void> {
  return new Promise((resolve, reject) => {
    if (child.exitCode != null || child.signalCode != null) {
      resolve();
      return;
    }
    const timeout = setTimeout(() => {
      child.off('exit', onExit);
      reject(new Error('Timed out waiting for process exit'));
    }, timeoutMs);
    const onExit = (): void => {
      clearTimeout(timeout);
      resolve();
    };
    child.once('exit', onExit);
  });
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
