import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
import { TeamProvisioningService } from '@main/services/team/TeamProvisioningService';
import { spawnCli } from '@main/utils/childProcess';
import { setAppDataBasePath } from '@main/utils/pathDecoder';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const hoisted = vi.hoisted(() => ({
  paths: {
    claudeRoot: '',
    teamsBase: '',
    tasksBase: '',
  },
}));

let tempClaudeRoot = '';
let tempTeamsBase = '';
let tempTasksBase = '';

vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
  ClaudeBinaryResolver: { resolve: vi.fn() },
}));

vi.mock('@main/utils/childProcess', () => ({
  execCli: vi.fn(async (_binaryPath: string | null, args: string[]) => {
    if (args[0] === '-e' && args[1]?.includes('process.execPath')) {
      return {
        stdout: JSON.stringify({ execPath: process.execPath, version: process.versions.node }),
        stderr: '',
      };
    }
    if (args.includes('model') && args.includes('list')) {
      return {
        stdout: JSON.stringify({
          schemaVersion: 1,
          providers: {
            codex: {
              defaultModel: 'gpt-5.4',
              models: [{ id: 'gpt-5.4', label: 'GPT-5.4', description: 'Codex default' }],
            },
          },
        }),
        stderr: '',
      };
    }
    if (args.includes('runtime') && args.includes('status')) {
      return {
        stdout: JSON.stringify({
          providers: {
            codex: {
              runtimeCapabilities: {
                modelCatalog: { dynamic: false, source: 'runtime' },
                reasoningEffort: {
                  supported: true,
                  values: ['low', 'medium', 'high'],
                  configPassthrough: false,
                },
              },
            },
          },
        }),
        stderr: '',
      };
    }
    return { stdout: '', stderr: '' };
  }),
  spawnCli: vi.fn(),
  killProcessTree: vi.fn(),
}));

vi.mock('@main/utils/shellEnv', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@main/utils/shellEnv')>();
  return {
    ...actual,
    getCachedShellEnv: () => ({ PATH: process.env.PATH ?? '', HOME: hoisted.paths.claudeRoot }),
    getShellPreferredHome: () => hoisted.paths.claudeRoot || actual.getShellPreferredHome(),
    resolveInteractiveShellEnv: vi.fn(async () => ({
      PATH: process.env.PATH ?? '',
      HOME: hoisted.paths.claudeRoot,
    })),
  };
});

vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@main/utils/pathDecoder')>();
  return {
    ...actual,
    getAutoDetectedClaudeBasePath: () => hoisted.paths.claudeRoot,
    getClaudeBasePath: () => hoisted.paths.claudeRoot,
    getTeamsBasePath: () => hoisted.paths.teamsBase,
    getTasksBasePath: () => hoisted.paths.tasksBase,
  };
});

type BootstrapSpec = {
  members?: Array<{
    name?: string;
    provider?: string;
    model?: string;
    mcpConfigPath?: string;
    mcpSettingSources?: string;
    strictMcpConfig?: boolean;
  }>;
};
type BootstrapSpecMember = NonNullable<BootstrapSpec['members']>[number];

type ProvisioningServiceOverrides = {
  buildProvisioningEnv: () => Promise<{
    env: Record<string, string>;
    authSource: string;
    geminiRuntimeAuth: null;
    providerArgs: string[];
  }>;
  resolveProviderDefaultModel: () => Promise<string>;
  normalizeTeamConfigForLaunch: () => Promise<void>;
  updateConfigProjectPath: () => Promise<void>;
  restorePrelaunchConfig: () => Promise<void>;
  assertConfigLeadOnlyForLaunch: () => Promise<void>;
  persistLaunchStateSnapshot: () => Promise<void>;
  validateAgentTeamsMcpRuntime: () => Promise<void>;
  pathExists: () => Promise<boolean>;
  startFilesystemMonitor: () => void;
};

function createFakeChild() {
  const child = Object.assign(new EventEmitter(), {
    pid: 12345,
    stdin: Object.assign(new EventEmitter(), {
      writable: true,
      write: vi.fn((_data: unknown, cb?: (err?: Error | null) => void) => {
        if (typeof cb === 'function') cb(null);
        return true;
      }),
      end: vi.fn(),
      on: vi.fn(),
      unref: vi.fn(),
    }),
    stdout: Object.assign(new EventEmitter(), {
      pipe: vi.fn(),
      unref: vi.fn(),
    }),
    stderr: Object.assign(new EventEmitter(), {
      pipe: vi.fn(),
      unref: vi.fn(),
    }),
    kill: vi.fn(),
    unref: vi.fn(),
  });
  return child;
}

function extractBootstrapSpec(): BootstrapSpec {
  const args = vi.mocked(spawnCli).mock.calls[0]?.[1] as string[] | undefined;
  const specFlagIndex = args?.indexOf('--team-bootstrap-spec') ?? -1;
  const specPath = specFlagIndex >= 0 ? args?.[specFlagIndex + 1] : null;
  if (!specPath) {
    throw new Error('Failed to extract bootstrap spec path from spawn args');
  }
  return JSON.parse(fs.readFileSync(specPath, 'utf8')) as BootstrapSpec;
}

function extractMcpConfigPathFromArgs(args: string[]): string {
  const mcpFlagIndex = args.indexOf('--mcp-config');
  if (mcpFlagIndex < 0 || !args[mcpFlagIndex + 1]) {
    throw new Error('Failed to extract MCP config path from launch args');
  }
  return args[mcpFlagIndex + 1];
}

function configureLaunchStubs(svc: TeamProvisioningService): void {
  const overrides = svc as unknown as ProvisioningServiceOverrides;
  overrides.buildProvisioningEnv = vi.fn(async () => ({
    env: { PATH: '/usr/bin', CLAUDE_TEAM_CONTROL_URL: 'http://127.0.0.1:43123' },
    authSource: 'codex_runtime',
    geminiRuntimeAuth: null,
    providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
  }));
  overrides.resolveProviderDefaultModel = vi.fn(async () => 'gpt-5.4');
  overrides.normalizeTeamConfigForLaunch = vi.fn(async () => {});
  overrides.updateConfigProjectPath = vi.fn(async () => {});
  overrides.restorePrelaunchConfig = vi.fn(async () => {});
  overrides.assertConfigLeadOnlyForLaunch = vi.fn(async () => {});
  overrides.persistLaunchStateSnapshot = vi.fn(async () => {});
  overrides.validateAgentTeamsMcpRuntime = vi.fn(async () => {});
  overrides.pathExists = vi.fn(async () => false);
  overrides.startFilesystemMonitor = vi.fn();
}

function writeProjectMcpConfig(projectDir: string): void {
  fs.writeFileSync(
    path.join(projectDir, '.mcp.json'),
    JSON.stringify({
      mcpServers: {
        tavily: { command: 'node', args: ['tavily.js'] },
        'brave-real-browser': { command: 'node', args: ['brave.js'] },
      },
    }),
    'utf8'
  );
}

function expectAppOnlyMcpConfigPath(mcpConfigPath: string): void {
  const memberMcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8')) as {
    mcpServers: Record<
      string,
      { command?: string; args?: string[]; enabled?: boolean; env?: Record<string, string> }
    >;
  };
  expect(Object.keys(memberMcpConfig.mcpServers)).toEqual(['agent-teams']);
  expect(memberMcpConfig.mcpServers['agent-teams']).toMatchObject({
    enabled: true,
    env: {
      AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
      CLAUDE_TEAM_CONTROL_URL: 'http://127.0.0.1:43123',
    },
  });
  expect(memberMcpConfig.mcpServers.tavily).toBeUndefined();
  expect(memberMcpConfig.mcpServers['brave-real-browser']).toBeUndefined();
}

function expectAppOnlyMemberMcpConfig(member: BootstrapSpecMember | undefined): void {
  expect(member?.mcpConfigPath).toEqual(expect.any(String));
  expectAppOnlyMcpConfigPath(member?.mcpConfigPath ?? '');
}

async function expectPathRemovedEventually(targetPath: string): Promise<void> {
  for (let attempt = 0; attempt < 20; attempt += 1) {
    if (!fs.existsSync(targetPath)) {
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, 25));
  }
  expect(fs.existsSync(targetPath)).toBe(false);
}

function writeCodexTeamWithAppOnlyMeta(teamName: string): void {
  const teamDir = path.join(tempTeamsBase, teamName);
  fs.mkdirSync(teamDir, { recursive: true });
  fs.writeFileSync(
    path.join(teamDir, 'config.json'),
    JSON.stringify({
      name: teamName,
      members: [
        { name: 'team-lead', agentType: 'team-lead', providerId: 'codex' },
        { name: 'alice', agentType: 'teammate', role: 'developer', providerId: 'codex' },
      ],
    }),
    'utf8'
  );
  fs.writeFileSync(
    path.join(teamDir, 'members.meta.json'),
    JSON.stringify({
      version: 1,
      providerBackendId: 'codex-native',
      members: [
        {
          name: 'alice',
          role: 'developer',
          providerId: 'codex',
          model: 'gpt-5.4',
          mcpPolicy: { mode: 'appOnly' },
        },
      ],
    }),
    'utf8'
  );
}

describe('TeamProvisioningService member MCP config safe e2e', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    tempClaudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude team member mcp-'));
    tempTeamsBase = path.join(tempClaudeRoot, 'teams');
    tempTasksBase = path.join(tempClaudeRoot, 'tasks');
    hoisted.paths.claudeRoot = tempClaudeRoot;
    hoisted.paths.teamsBase = tempTeamsBase;
    hoisted.paths.tasksBase = tempTasksBase;
    setAppDataBasePath(tempClaudeRoot);
    fs.mkdirSync(tempTeamsBase, { recursive: true });
    fs.mkdirSync(tempTasksBase, { recursive: true });
  });

  afterEach(() => {
    setAppDataBasePath(null);
    fs.rmSync(tempClaudeRoot, { recursive: true, force: true });
  });

  it('createTeam preserves request Agent Teams MCP only in the real member MCP config', async () => {
    const teamName = 'codex-app-only-create';
    const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex app-only project-'));
    writeProjectMcpConfig(projectDir);

    vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
    vi.mocked(spawnCli).mockReturnValue(createFakeChild() as never);

    const svc = new TeamProvisioningService();
    configureLaunchStubs(svc);
    svc.setControlApiBaseUrlResolver(async () => 'http://127.0.0.1:43123');

    let runId: string | undefined;
    try {
      const created = await svc.createTeam(
        {
          teamName,
          cwd: projectDir,
          providerId: 'codex',
          providerBackendId: 'codex-native',
          model: 'gpt-5.4',
          members: [
            {
              name: 'alice',
              role: 'developer',
              providerId: 'codex',
              model: 'gpt-5.4',
              mcpPolicy: { mode: 'appOnly' },
            },
          ],
        },
        () => {}
      );
      runId = created.runId;

      const member = extractBootstrapSpec().members?.[0];
      expect(member).toEqual(
        expect.objectContaining({
          name: 'alice',
          provider: 'codex',
          model: 'gpt-5.4',
          mcpSettingSources: 'user,project,local',
          strictMcpConfig: true,
        })
      );
      expectAppOnlyMemberMcpConfig(member);

      const memberMcpConfigPath = member?.mcpConfigPath ?? '';
      await svc.cancelProvisioning(runId);
      runId = undefined;
      await expectPathRemovedEventually(memberMcpConfigPath);
    } finally {
      if (runId) {
        await svc.cancelProvisioning(runId);
      }
      fs.rmSync(projectDir, { recursive: true, force: true });
    }
  });

  it('launchTeam preserves members.meta Agent Teams MCP only in the real member MCP config', async () => {
    const teamName = 'codex-app-only-meta-launch';
    const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-app-only-project-'));
    writeCodexTeamWithAppOnlyMeta(teamName);
    writeProjectMcpConfig(projectDir);

    vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
    vi.mocked(spawnCli).mockReturnValue(createFakeChild() as never);

    const svc = new TeamProvisioningService();
    configureLaunchStubs(svc);
    svc.setControlApiBaseUrlResolver(async () => 'http://127.0.0.1:43123');

    let runId: string | undefined;
    try {
      const launched = await svc.launchTeam(
        {
          teamName,
          cwd: projectDir,
          providerId: 'codex',
          providerBackendId: 'codex-native',
          model: 'gpt-5.4',
          clearContext: true,
        },
        () => {}
      );
      runId = launched.runId;

      const member = extractBootstrapSpec().members?.[0];
      expect(member).toEqual(
        expect.objectContaining({
          name: 'alice',
          provider: 'codex',
          model: 'gpt-5.4',
          mcpSettingSources: 'user,project,local',
          strictMcpConfig: true,
        })
      );
      expectAppOnlyMemberMcpConfig(member);

      const memberMcpConfigPath = member?.mcpConfigPath ?? '';
      await svc.cancelProvisioning(runId);
      runId = undefined;
      await expectPathRemovedEventually(memberMcpConfigPath);
    } finally {
      if (runId) {
        await svc.cancelProvisioning(runId);
      }
      fs.rmSync(projectDir, { recursive: true, force: true });
    }
  });

  it('live add-member MCP prep writes and discards a real Agent Teams MCP only config', async () => {
    const teamName = 'codex-app-only-live-add';
    const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex live add project-'));
    writeProjectMcpConfig(projectDir);

    vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
    vi.mocked(spawnCli).mockReturnValue(createFakeChild() as never);

    const svc = new TeamProvisioningService();
    configureLaunchStubs(svc);
    svc.setControlApiBaseUrlResolver(async () => 'http://127.0.0.1:43123');

    let runId: string | undefined;
    let liveMcpConfigPath = '';
    try {
      const created = await svc.createTeam(
        {
          teamName,
          cwd: projectDir,
          providerId: 'codex',
          providerBackendId: 'codex-native',
          model: 'gpt-5.4',
          members: [{ name: 'alice', role: 'developer', providerId: 'codex', model: 'gpt-5.4' }],
        },
        () => {}
      );
      runId = created.runId;
      (svc as unknown as { aliveRunByTeam: Map<string, string> }).aliveRunByTeam.set(
        teamName,
        runId
      );

      const liveMcpConfig = await svc.prepareLiveMemberMcpLaunchConfig({
        teamName,
        cwd: projectDir,
        mcpPolicy: { mode: 'appOnly' },
      });

      expect(liveMcpConfig).toEqual(
        expect.objectContaining({
          mcpConfigPath: expect.any(String),
          mcpSettingSources: 'user,project,local',
          strictMcpConfig: true,
        })
      );
      liveMcpConfigPath = liveMcpConfig?.mcpConfigPath ?? '';
      expectAppOnlyMcpConfigPath(liveMcpConfigPath);

      await svc.discardLiveMemberMcpLaunchConfig({
        teamName,
        mcpLaunchConfig: liveMcpConfig,
      });
      await expectPathRemovedEventually(liveMcpConfigPath);

      await svc.cancelProvisioning(runId);
      runId = undefined;
    } finally {
      if (runId) {
        await svc.cancelProvisioning(runId);
      }
      fs.rmSync(projectDir, { recursive: true, force: true });
    }
  });

  it('restartMember direct process preserves Agent Teams MCP only in the real restart args', async () => {
    const teamName = 'codex-app-only-process-restart';
    const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex restart project-'));
    writeProjectMcpConfig(projectDir);

    vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
    vi.mocked(spawnCli).mockReturnValue(createFakeChild() as never);

    const svc = new TeamProvisioningService();
    configureLaunchStubs(svc);

    let runId: string | undefined;
    let restartMcpConfigPath = '';
    try {
      const created = await svc.createTeam(
        {
          teamName,
          cwd: projectDir,
          providerId: 'codex',
          providerBackendId: 'codex-native',
          model: 'gpt-5.4',
          members: [
            {
              name: 'alice',
              role: 'developer',
              providerId: 'codex',
              model: 'gpt-5.4',
              mcpPolicy: { mode: 'appOnly' },
            },
          ],
        },
        () => {}
      );
      runId = created.runId;
      (svc as unknown as { aliveRunByTeam: Map<string, string> }).aliveRunByTeam.set(
        teamName,
        runId
      );

      (svc as unknown as { readConfigForStrictDecision: () => Promise<unknown> }).readConfigForStrictDecision =
        vi.fn(async () => ({
          name: teamName,
          projectPath: projectDir,
          members: [
            { name: 'team-lead', agentType: 'team-lead', providerId: 'codex' },
            {
              name: 'alice',
              role: 'developer',
              providerId: 'codex',
              model: 'gpt-5.4',
              mcpPolicy: { mode: 'appOnly' },
            },
          ],
        }));
      (svc as unknown as { readPersistedRuntimeMembers: () => unknown[] }).readPersistedRuntimeMembers =
        vi.fn(() => [{ name: 'alice', backendType: 'process', cwd: projectDir }]);
      (
        svc as unknown as { getLiveTeamAgentRuntimeMetadata: () => Promise<Map<string, unknown>> }
      ).getLiveTeamAgentRuntimeMetadata = vi.fn(async () => new Map());
      (
        svc as unknown as {
          buildTeamRuntimeLaunchArgsPlan: () => Promise<{
            fastModeArgs: string[];
            runtimeTurnSettledHookArgs: string[];
            providerArgs: string[];
            settingsArgs: string[];
            extraArgs: string[];
            inheritedProviderArgs: string[];
            appManagedSettingsPath: string | null;
          }>;
        }
      ).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({
        fastModeArgs: [],
        runtimeTurnSettledHookArgs: [],
        providerArgs: [],
        settingsArgs: [],
        extraArgs: [],
        inheritedProviderArgs: [],
        appManagedSettingsPath: null,
      }));
      (
        svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise<void> }
      ).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {});
      (svc as unknown as { enqueueDirectRestartPrompt: () => void }).enqueueDirectRestartPrompt =
        vi.fn();

      vi.mocked(spawnCli).mockClear();
      await svc.restartMember(teamName, 'alice');

      const restartArgs = vi.mocked(spawnCli).mock.calls[0]?.[1] as string[] | undefined;
      expect(restartArgs).toEqual(
        expect.arrayContaining([
          '--teammate-runtime',
          'headless',
          '--setting-sources',
          'user,project,local',
          '--strict-mcp-config',
        ])
      );
      restartMcpConfigPath = extractMcpConfigPathFromArgs(restartArgs ?? []);
      expectAppOnlyMcpConfigPath(restartMcpConfigPath);

      await svc.cancelProvisioning(runId);
      runId = undefined;
      await expectPathRemovedEventually(restartMcpConfigPath);
    } finally {
      if (runId) {
        await svc.cancelProvisioning(runId);
      }
      fs.rmSync(projectDir, { recursive: true, force: true });
    }
  });
});
