import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
  CodexRawConfigConflictError,
  CodexRawConfigValidationError,
  getCodexDashboardDiagnostics,
  getCodexRawConfig,
  patchCodexConfig,
  resolveCodexConfigPaths,
  saveCodexRawConfig,
  summarizeCodexFeatureFlags,
  summarizeCodexMcpServers,
  summarizeCodexModelProviders,
  summarizeCodexProjectTrust,
} from '../../../src/web-server/services/codex-dashboard-service';

const testRoot = path.join(os.tmpdir(), `ccs-codex-dashboard-test-${Date.now()}`);
const codexHome = path.join(testRoot, '.codex-home');
const codexStubPath = path.join(testRoot, 'codex');

function writeCodexStub(options?: {
  helpText?: string;
  version?: string;
  supportsConfigOverrides?: boolean;
}) {
  const helpText =
    options?.helpText ?? '  -c, --config <key=value>\n  -p, --profile <CONFIG_PROFILE>\n';
  const version = options?.version ?? 'codex-cli 0.118.0-alpha.3';
  const supportsConfigOverrides = options?.supportsConfigOverrides ?? true;

  fs.writeFileSync(
    codexStubPath,
    `#!/bin/sh
if [ "$1" = "-c" ] || [ "$1" = "--config" ]; then
  if [ "${supportsConfigOverrides ? '1' : '0'}" != "1" ]; then
    printf '%s\\n' 'codex: unknown option --config' >&2
    exit 1
  fi
  if [ "$3" = "--version" ] || [ "$3" = "-v" ]; then
    printf '%s\\n' "${version}"
    exit 0
  fi
fi
if [ "$1" = "--version" ]; then
  printf '%s\\n' "${version}"
  exit 0
fi
if [ "$1" = "--help" ]; then
  printf '%s' "${helpText}"
  exit 0
fi
exit 0
`
  );
  fs.chmodSync(codexStubPath, 0o755);
}

beforeEach(() => {
  fs.mkdirSync(testRoot, { recursive: true });
  fs.mkdirSync(codexHome, { recursive: true });
  writeCodexStub();
  process.env.CODEX_HOME = codexHome;
  process.env.CCS_CODEX_PATH = codexStubPath;
});

afterEach(() => {
  delete process.env.CODEX_HOME;
  delete process.env.CCS_CODEX_PATH;
  if (fs.existsSync(testRoot)) {
    fs.rmSync(testRoot, { recursive: true, force: true });
  }
});

describe('codex-dashboard-service', () => {
  it('resolves codex config paths with CODEX_HOME override', () => {
    const resolved = resolveCodexConfigPaths({
      env: {
        CODEX_HOME: './custom-codex-home',
      } as NodeJS.ProcessEnv,
      homeDir: '/Users/tester',
    });

    expect(resolved.baseDir).toBe(path.resolve('./custom-codex-home'));
    expect(resolved.baseDirDisplay).toBe('$CODEX_HOME');
    expect(resolved.configPath).toBe(path.join(path.resolve('./custom-codex-home'), 'config.toml'));
    expect(resolved.configDisplayPath).toBe('$CODEX_HOME/config.toml');
  });

  it('summarizes model providers with auth and header metadata', () => {
    const summary = summarizeCodexModelProviders({
      cliproxy: {
        base_url: 'http://127.0.0.1:8317/api/provider/codex',
        env_key: 'CLIPROXY_API_KEY',
        wire_api: 'responses',
        http_headers: { 'x-test': '1' },
      },
      local: {
        base_url: 'http://localhost:11434/v1',
        experimental_bearer_token: 'secret',
        supports_websockets: true,
      },
    });

    expect(summary.length).toBe(2);
    expect(summary[0].name).toBe('cliproxy');
    expect(summary[0].envKey).toBe('CLIPROXY_API_KEY');
    expect(summary[0].hasHttpHeaders).toBe(true);
    expect(summary[1].usesExperimentalBearerToken).toBe(true);
  });

  it('summarizes feature flags, project trust, and mcp servers', () => {
    const features = summarizeCodexFeatureFlags({
      multi_agent: true,
      shell_snapshot: false,
      custom_mode: 'beta',
    });
    const projects = summarizeCodexProjectTrust({
      '/tmp/a': { trust_level: 'trusted' },
      '/tmp/b': { trust_level: 'untrusted' },
    });
    const servers = summarizeCodexMcpServers({
      stdio: {
        command: 'npx',
        enabled_tools: ['browser_snapshot'],
      },
      remote: {
        url: 'https://example.test/mcp',
        bearer_token: 'not-allowed-inline',
        required: true,
      },
    });

    expect(features.enabled.map((feature) => feature.name)).toEqual(['multi_agent']);
    expect(features.disabled.map((feature) => feature.name)).toEqual(['shell_snapshot']);
    expect(features.all.find((feature) => feature.name === 'custom_mode')?.state).toBe('custom');
    expect(projects.length).toBe(2);
    expect(projects[0].trustLevel).toBe('trusted');
    expect(servers[0].transport).toBe('streamable-http');
    expect(servers[0].usesInlineBearerToken).toBe(true);
  });

  it('returns raw config payload for missing config.toml', async () => {
    const raw = await getCodexRawConfig();

    expect(raw.exists).toBe(false);
    expect(raw.path).toBe('$CODEX_HOME/config.toml');
    expect(raw.rawText).toBe('');
    expect(raw.config).toBeNull();
    expect(raw.readError).toBeNull();
  });

  it('returns parseError when config.toml is invalid TOML', async () => {
    fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n[features\n');

    const raw = await getCodexRawConfig();

    expect(raw.exists).toBe(true);
    expect(raw.parseError).toBeString();
    expect(raw.config).toBeNull();
  });

  it('returns readError when config.toml is a symlink', async () => {
    const configPath = path.join(codexHome, 'config.toml');
    const targetPath = path.join(testRoot, 'linked.toml');
    fs.writeFileSync(targetPath, 'model = "gpt-5.4"\n');
    fs.symlinkSync(targetPath, configPath);

    const raw = await getCodexRawConfig();

    expect(raw.exists).toBe(true);
    expect(raw.readError).toContain('Refusing symlink file');
    expect(raw.config).toBeNull();
  });

  it('includes docs links, support matrix, and config summaries in diagnostics', async () => {
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      `model = "gpt-5.4"
profile = "work"
model_context_window = 800000
model_auto_compact_token_limit = 700000
model_provider = "cliproxy"
approval_policy = "never"
sandbox_mode = "danger-full-access"
web_search = "live"

[features]
multi_agent = true
shell_snapshot = false
runtime_metrics = true

[model_providers.cliproxy]
name = "CLIProxyAPI"
base_url = "http://127.0.0.1:8317/api/provider/codex"
env_key = "CLIPROXY_API_KEY"
wire_api = "responses"

[projects."/tmp/project-a"]
trust_level = "trusted"

[projects."/tmp/project-b"]
trust_level = "untrusted"

[mcp_servers.playwright]
command = "npx"
args = ["@playwright/mcp@latest"]
enabled_tools = ["browser_snapshot"]
tool_timeout_sec = 30

[profiles.work]
model = "gpt-5.4"
`
    );

    const diagnostics = await getCodexDashboardDiagnostics();

    expect(diagnostics.binary.installed).toBe(true);
    expect(diagnostics.binary.supportsConfigOverrides).toBe(true);
    expect(diagnostics.config.model).toBe('gpt-5.4');
    expect(diagnostics.config.modelContextWindow).toBe(800000);
    expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000);
    expect(diagnostics.config.activeProfile).toBe('work');
    expect(diagnostics.config.modelProvider).toBe('cliproxy');
    expect(diagnostics.config.profileCount).toBe(1);
    expect(diagnostics.config.modelProviderCount).toBe(1);
    expect(diagnostics.config.featureCount).toBe(3);
    expect(diagnostics.config.enabledFeatures.map((feature) => feature.name)).toEqual([
      'multi_agent',
      'runtime_metrics',
    ]);
    expect(diagnostics.config.trustedProjectCount).toBe(1);
    expect(diagnostics.config.untrustedProjectCount).toBe(1);
    expect(diagnostics.config.mcpServerCount).toBe(1);
    expect(diagnostics.docsReference.links.length).toBeGreaterThan(0);
    expect(diagnostics.supportMatrix.some((entry) => entry.id === 'default')).toBe(true);
  });

  it('warns when the active model provider is selected without base_url or env_key', async () => {
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      `model_provider = "cliproxy"

[model_providers.cliproxy]
wire_api = "responses"
`
    );

    const diagnostics = await getCodexDashboardDiagnostics();

    expect(diagnostics.warnings.some((warning) => warning.includes('missing base_url'))).toBe(true);
    expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(true);
  });

  it('does not warn for built-in openai provider without a custom model_providers entry', async () => {
    fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model_provider = "openai"\n');

    const diagnostics = await getCodexDashboardDiagnostics();

    expect(diagnostics.warnings.some((warning) => warning.includes('missing from [model_providers]'))).toBe(
      false
    );
  });

  it('allows custom providers that use requires_openai_auth without env_key warnings', async () => {
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      `model_provider = "corp-openai"

[model_providers.corp-openai]
base_url = "https://example.test/v1"
requires_openai_auth = true
`
    );

    const diagnostics = await getCodexDashboardDiagnostics();

    expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(false);
  });

  it('summarizes granular approval policies without flattening them to null', async () => {
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      'approval_policy = { granular = { edit = "on-request" } }\n'
    );

    const diagnostics = await getCodexDashboardDiagnostics();

    expect(diagnostics.config.approvalPolicy).toBe('granular (custom)');
  });

  it('warns when active profile is missing, config overrides are unavailable, or risky fields exist', async () => {
    writeCodexStub({
      helpText: '  -p, --profile <CONFIG_PROFILE>\n',
      supportsConfigOverrides: false,
    });
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      `profile = "missing-profile"

[model_providers.local]
experimental_bearer_token = "secret"

[mcp_servers.remote]
url = "https://example.test/mcp"
bearer_token = "secret"
`
    );

    const diagnostics = await getCodexDashboardDiagnostics();

    expect(
      diagnostics.warnings.some((warning) => warning.includes('does not expose --config overrides'))
    ).toBe(true);
    expect(
      diagnostics.warnings.some((warning) => warning.includes('missing from [profiles]'))
    ).toBe(true);
    expect(
      diagnostics.warnings.some((warning) => warning.includes('experimental_bearer_token'))
    ).toBe(true);
    expect(diagnostics.warnings.some((warning) => warning.includes('inline bearer_token'))).toBe(
      true
    );
    expect(
      diagnostics.supportMatrix.find((entry) => entry.id === 'cliproxy-provider-codex')?.supported
    ).toBe(false);
    expect(
      diagnostics.supportMatrix.find((entry) => entry.id === 'settings-with-bridge')?.supported
    ).toBe(false);
  });

  it('saves valid raw config content', async () => {
    const result = await saveCodexRawConfig({
      rawText: 'model = "gpt-5.4"\n[features]\nmulti_agent = true\n',
    });

    const written = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');

    expect(result.success).toBe(true);
    expect(result.mtime).toBeGreaterThan(0);
    expect(written).toContain('model = "gpt-5.4"');
  });

  it('rejects invalid TOML while saving raw config', async () => {
    await expect(saveCodexRawConfig({ rawText: 'model = "gpt-5.4"\n[features\n' })).rejects.toThrow(
      CodexRawConfigValidationError
    );
  });

  it('rejects stale writes with conflict error', async () => {
    const configPath = path.join(codexHome, 'config.toml');
    fs.writeFileSync(configPath, 'model = "gpt-5.4"\n');

    await expect(
      saveCodexRawConfig({
        rawText: 'model = "gpt-5.3-codex"\n',
        expectedMtime: 1,
      })
    ).rejects.toThrow(CodexRawConfigConflictError);
  });

  it('rejects writes when expectedMtime differs by even 1ms', async () => {
    const configPath = path.join(codexHome, 'config.toml');
    fs.writeFileSync(configPath, 'model = "gpt-5.4"\n');

    const current = await getCodexRawConfig();

    await expect(
      saveCodexRawConfig({
        rawText: 'model = "gpt-5.4"\nprofile = "work"\n',
        expectedMtime: current.mtime + 1,
      })
    ).rejects.toThrow(CodexRawConfigConflictError);
  });

  it('patches top-level settings and project trust through structured controls', async () => {
    const result = await patchCodexConfig({
      kind: 'top-level',
      values: {
        model: 'gpt-5.4',
        modelReasoningEffort: 'high',
        modelContextWindow: 800000,
        modelAutoCompactTokenLimit: 700000,
        approvalPolicy: 'never',
        sandboxMode: 'workspace-write',
        webSearch: 'cached',
        toolOutputTokenLimit: 12000,
        personality: 'friendly',
      },
    });

    await patchCodexConfig({
      kind: 'project-trust',
      path: '/tmp/workspace-a',
      trustLevel: 'trusted',
      expectedMtime: result.mtime,
    });

    const diagnostics = await getCodexDashboardDiagnostics();
    expect(diagnostics.config.model).toBe('gpt-5.4');
    expect(diagnostics.config.modelReasoningEffort).toBe('high');
    expect(diagnostics.config.modelContextWindow).toBe(800000);
    expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000);
    expect(diagnostics.config.toolOutputTokenLimit).toBe(12000);
    expect(diagnostics.config.personality).toBe('friendly');
    expect(diagnostics.config.projectTrust[0]?.path).toBe('/tmp/workspace-a');
    expect(result.rawText).toContain('model = "gpt-5.4"');
    expect(result.rawText).toContain('model_context_window = 800000');
    expect(result.rawText).toContain('model_auto_compact_token_limit = 700000');
    expect(result.config?.model).toBe('gpt-5.4');
  });

  it('allows structured patches on existing config.toml even when expectedMtime is omitted', async () => {
    fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n');

    const result = await patchCodexConfig({
      kind: 'feature',
      feature: 'multi_agent',
      enabled: true,
    });

    expect(result.rawText).toContain('model = "gpt-5.4"');
    expect(result.rawText).toContain('[features]');
    expect(result.rawText).toContain('multi_agent = true');
    expect(result.config?.features).toEqual({ multi_agent: true });
  });

  it('preserves unsupported approval_policy objects when structured saves touch other fields', async () => {
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      'model = "gpt-5.4"\napproval_policy = { granular = { edit = "on-request" } }\n'
    );

    const current = await getCodexRawConfig();
    const result = await patchCodexConfig({
      kind: 'top-level',
      expectedMtime: current.mtime,
      values: {
        model: 'gpt-5.4-mini',
        approvalPolicy: null,
      },
    });

    expect(result.rawText).toContain('model = "gpt-5.4-mini"');
    expect(result.rawText).toContain('[approval_policy.granular]');
    expect(result.rawText).toContain('edit = "on-request"');
    expect(result.config?.approval_policy).toEqual({
      granular: { edit: 'on-request' },
    });
  });

  it('expands home paths for project trust and rejects relative paths', async () => {
    const homeWorkspacePath = path.join(os.homedir(), 'codex-workspace');
    const expanded = await patchCodexConfig({
      kind: 'project-trust',
      path: '~/codex-workspace',
      trustLevel: 'trusted',
    });

    expect(expanded.rawText).toContain(`[projects."${homeWorkspacePath}"]`);

    await expect(
      patchCodexConfig({
        kind: 'project-trust',
        path: './relative-workspace',
        trustLevel: 'trusted',
      })
    ).rejects.toThrow(CodexRawConfigValidationError);
  });

  it('patches profiles, providers, and mcp servers through structured controls', async () => {
    const providerResult = await patchCodexConfig({
      kind: 'model-provider',
      action: 'upsert',
      name: 'cliproxy',
      values: {
        displayName: 'CLIProxy',
        baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
        envKey: 'CLIPROXY_API_KEY',
        wireApi: 'responses',
      },
    });

    const profileResult = await patchCodexConfig({
      kind: 'profile',
      action: 'upsert',
      name: 'deep-review',
      values: {
        model: 'gpt-5.4',
        modelProvider: 'cliproxy',
        modelReasoningEffort: 'xhigh',
      },
      setAsActive: true,
      expectedMtime: providerResult.mtime,
    });

    await patchCodexConfig({
      kind: 'mcp-server',
      action: 'upsert',
      name: 'playwright',
      values: {
        transport: 'stdio',
        command: 'npx',
        args: ['@playwright/mcp@latest'],
        enabled: true,
        required: false,
        startupTimeoutSec: 15,
        toolTimeoutSec: 30,
      },
      expectedMtime: profileResult.mtime,
    });

    const diagnostics = await getCodexDashboardDiagnostics();
    expect(diagnostics.config.activeProfile).toBe('deep-review');
    expect(diagnostics.config.modelProviderCount).toBe(1);
    expect(diagnostics.config.mcpServerCount).toBe(1);

    const raw = await getCodexRawConfig();
    expect(raw.rawText).toContain('[profiles.deep-review]');
    expect(raw.rawText).toContain('[model_providers.cliproxy]');
    expect(raw.rawText).toContain('[mcp_servers.playwright]');
    expect(profileResult.rawText).toContain('[profiles.deep-review]');
    expect(profileResult.config?.profile).toBe('deep-review');
  });

  it('accepts non-integer MCP timeout values documented by upstream Codex', async () => {
    const result = await patchCodexConfig({
      kind: 'mcp-server',
      action: 'upsert',
      name: 'streaming',
      values: {
        transport: 'stdio',
        command: 'npx',
        startupTimeoutSec: 1.5,
        toolTimeoutSec: 2.25,
      },
    });

    expect(result.rawText).toContain('startup_timeout_sec = 1.5');
    expect(result.rawText).toContain('tool_timeout_sec = 2.25');
  });

  it('rewrites legacy startup_timeout_ms keys when editing MCP server timeouts', async () => {
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      ['[mcp_servers.streaming]', 'command = "npx"', 'startup_timeout_ms = 1500', ''].join('\n')
    );
    const raw = await getCodexRawConfig();

    const result = await patchCodexConfig({
      kind: 'mcp-server',
      action: 'upsert',
      name: 'streaming',
      expectedMtime: raw.mtime,
      values: {
        transport: 'stdio',
        command: 'npx',
        startupTimeoutSec: 2.5,
      },
    });

    expect(result.rawText).toContain('startup_timeout_sec = 2.5');
    expect(result.rawText).not.toContain('startup_timeout_ms');
  });

  it('patches streamable-http mcp servers through structured controls', async () => {
    const result = await patchCodexConfig({
      kind: 'mcp-server',
      action: 'upsert',
      name: 'remote',
      values: {
        transport: 'streamable-http',
        url: 'https://example.test/mcp',
        enabled: true,
        required: true,
        toolTimeoutSec: 45,
        enabledTools: ['browser_snapshot'],
        disabledTools: ['slow_tool'],
      },
    });

    expect(result.rawText).toContain('[mcp_servers.remote]');
    expect(result.rawText).toContain('url = "https://example.test/mcp"');
    expect(result.rawText).toContain('required = true');

    const diagnostics = await getCodexDashboardDiagnostics();
    expect(diagnostics.config.mcpServers).toEqual([
      expect.objectContaining({
        name: 'remote',
        transport: 'streamable-http',
        required: true,
        toolTimeoutSec: 45,
        enabledToolsCount: 1,
        disabledToolsCount: 1,
      }),
    ]);
  });

  it('rejects structured patches when config.toml is invalid', async () => {
    fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n[features\n');

    await expect(
      patchCodexConfig({
        kind: 'feature',
        feature: 'multi_agent',
        enabled: true,
      })
    ).rejects.toThrow(CodexRawConfigValidationError);
  });

  it('removes feature overrides when a feature is reset to inherited state', async () => {
    const enabled = await patchCodexConfig({
      kind: 'feature',
      feature: 'multi_agent',
      enabled: true,
    });

    expect(enabled.rawText).toContain('[features]');
    expect(enabled.rawText).toContain('multi_agent = true');

    const reset = await patchCodexConfig({
      kind: 'feature',
      feature: 'multi_agent',
      enabled: null,
      expectedMtime: enabled.mtime,
    });

    expect(reset.rawText).not.toContain('[features]');
    expect(reset.config?.features).toBeUndefined();
  });

  it('rejects malformed structured patch payloads at runtime', async () => {
    await expect(
      patchCodexConfig({
        kind: 'feature',
        feature: 'multi_agent',
        enabled: 'true' as unknown as boolean | null,
      })
    ).rejects.toThrow(CodexRawConfigValidationError);

    await expect(
      patchCodexConfig({
        kind: 'project-trust',
        path: '~/codex-workspace',
        trustLevel: 'always',
      })
    ).rejects.toThrow(CodexRawConfigValidationError);

    await expect(
      patchCodexConfig({
        kind: 'mcp-server',
        action: 'upsert',
        name: 'remote',
        values: {
          transport: 'http' as 'stdio' | 'streamable-http',
          url: 'https://example.test/mcp',
        },
      })
    ).rejects.toThrow(CodexRawConfigValidationError);
  });

  it('rejects invalid enum values even when they already exist in config.toml', async () => {
    fs.writeFileSync(
      path.join(codexHome, 'config.toml'),
      'model = "gpt-5.4"\napproval_policy = "legacy"\n'
    );

    await expect(
      patchCodexConfig({
        kind: 'top-level',
        values: {
          approvalPolicy: 'legacy' as unknown as 'on-request' | 'never' | 'untrusted' | null,
        },
      })
    ).rejects.toThrow(CodexRawConfigValidationError);
  });

  it('rejects invalid long-context values in structured top-level patches', async () => {
    await expect(
      patchCodexConfig({
        kind: 'top-level',
        values: {
          modelContextWindow: 0,
        },
      })
    ).rejects.toThrow(CodexRawConfigValidationError);

    await expect(
      patchCodexConfig({
        kind: 'top-level',
        values: {
          modelAutoCompactTokenLimit: 1.5,
        },
      })
    ).rejects.toThrow(CodexRawConfigValidationError);
  });
});
