import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import {
  evaluateBuiltInMcp,
  resolveBundledMcpNodeCommand,
  resolveCustomMcpRuntimeEntryForRuntime,
} from '../apps/desktop/src/main/runtime-mcp.ts'
import { clearConfigCaches } from '../apps/desktop/src/main/config-loader.ts'
import type { AppSettings } from '../packages/shared/src/index.ts'
import type { BundleMcp } from '../apps/desktop/src/main/config-loader.ts'

const BASE_SETTINGS: AppSettings = {
  selectedProviderId: null,
  selectedModelId: null,
  providerCredentials: {},
  integrationCredentials: {},
  integrationEnabled: {},
  bashPermission: 'deny',
  fileWritePermission: 'deny',
  enableBash: false,
  enableFileWrite: false,
  runtimeToolingBridgeEnabled: true,
  workflowLaunchAtLogin: false,
  workflowRunInBackground: false,
  workflowDesktopNotifications: true,
  workflowQuietHoursStart: '22:00',
  workflowQuietHoursEnd: '07:00',
}

function withConfigDir(configJson: Record<string, unknown>, fn: () => void) {
  const tempRoot = mkdtempSync(join(tmpdir(), 'opencowork-mcp-optin-'))
  const configDir = join(tempRoot, 'downstream')
  const userDataDir = join(tempRoot, 'user-data')
  mkdirSync(configDir, { recursive: true })
  mkdirSync(userDataDir, { recursive: true })
  writeFileSync(join(configDir, 'config.jsonc'), JSON.stringify(configJson))
  const previousConfigDir = process.env.OPEN_COWORK_CONFIG_DIR
  const previousUserDataDir = process.env.OPEN_COWORK_USER_DATA_DIR
  process.env.OPEN_COWORK_CONFIG_DIR = configDir
  process.env.OPEN_COWORK_USER_DATA_DIR = userDataDir
  clearConfigCaches()
  try {
    fn()
  } finally {
    if (previousConfigDir === undefined) delete process.env.OPEN_COWORK_CONFIG_DIR
    else process.env.OPEN_COWORK_CONFIG_DIR = previousConfigDir
    if (previousUserDataDir === undefined) delete process.env.OPEN_COWORK_USER_DATA_DIR
    else process.env.OPEN_COWORK_USER_DATA_DIR = previousUserDataDir
    clearConfigCaches()
    rmSync(tempRoot, { recursive: true, force: true })
  }
}

// Minimal valid config that the Cowork loader accepts. We layer a
// single `mcp` of interest on top via the caller's input.
function baseConfig(extra: Record<string, unknown>) {
  return {
    allowedEnvPlaceholders: [],
    providers: {
      available: ['test'],
      defaultProvider: 'test',
      defaultModel: 'fast',
      descriptors: {
        test: { name: 'Test', description: '', credentials: [], models: [{ id: 'fast', name: 'Fast' }] },
      },
    },
    ...extra,
  }
}

test('evaluateBuiltInMcp — local MCP with required credentials present is ready', () => {
  withConfigDir(baseConfig({}), () => {
    const mcp: BundleMcp = {
      name: 'perplexity',
      type: 'local',
      description: 'Perplexity search',
      authMode: 'api_token',
      command: ['node', '/tmp/perplexity.js'],
      credentials: [
        { key: 'apiKey', label: 'API key', description: '', required: true, secret: true },
      ],
      envSettings: [{ env: 'PERPLEXITY_API_KEY', key: 'apiKey' }],
    }
    const settings: AppSettings = {
      ...BASE_SETTINGS,
      integrationCredentials: { perplexity: { apiKey: 'sk-test' } },
    }
    const result = evaluateBuiltInMcp(mcp, settings)
    assert.equal(result.status, 'ready')
    if (result.status !== 'ready') return
    assert.equal(result.entry.type, 'local')
    if (result.entry.type !== 'local') return
    assert.equal(result.entry.environment?.PERPLEXITY_API_KEY, 'sk-test')
  })
})

test('resolveBundledMcpNodeCommand uses Electron as Node in packaged builds', () => {
  assert.deepEqual(resolveBundledMcpNodeCommand('/tmp/charts.js', {
    isPackaged: false,
    executablePath: '/Applications/Open Cowork.app/Contents/MacOS/Open Cowork',
  }), {
    command: ['node', '/tmp/charts.js'],
    environment: {},
  })

  assert.deepEqual(resolveBundledMcpNodeCommand('/tmp/charts.js', {
    isPackaged: true,
    executablePath: '/Applications/Open Cowork.app/Contents/MacOS/Open Cowork',
  }), {
    command: ['/Applications/Open Cowork.app/Contents/MacOS/Open Cowork', '/tmp/charts.js'],
    environment: { ELECTRON_RUN_AS_NODE: '1' },
  })
})

test('resolveCustomMcpRuntimeEntryForRuntime rejects public hostnames that resolve private', async () => {
  const entry = await resolveCustomMcpRuntimeEntryForRuntime({
    scope: 'machine',
    name: 'rebind',
    type: 'http',
    url: 'https://mcp.example.com/api',
  }, {
    resolveHostname: async () => [{ address: '169.254.169.254', family: 4 }],
  })

  assert.equal(entry, null)
})

test('evaluateBuiltInMcp — local MCP missing required credentials is skipped as not-configured', () => {
  // Perplexity's headline bug: the MCP would register in config.mcp,
  // fail to spawn because no key, and show up in the status log as
  // `perplexity=failed` for a user who simply hadn't added their key.
  withConfigDir(baseConfig({}), () => {
    const mcp: BundleMcp = {
      name: 'perplexity',
      type: 'local',
      description: 'Perplexity search',
      authMode: 'api_token',
      command: ['node', '/tmp/perplexity.js'],
      credentials: [
        { key: 'apiKey', label: 'API key', description: '', required: true, secret: true },
      ],
    }
    const result = evaluateBuiltInMcp(mcp, BASE_SETTINGS)
    assert.equal(result.status, 'skipped')
    if (result.status !== 'skipped') return
    assert.equal(result.reason, 'not-configured')
  })
})

test('evaluateBuiltInMcp — required conditional credentials only apply when their when clause matches', () => {
  withConfigDir(baseConfig({}), () => {
    const mcp: BundleMcp = {
      name: 'multi-auth',
      type: 'local',
      description: 'Multi-mode auth MCP',
      authMode: 'api_token',
      command: ['node', '/tmp/multi-auth.js'],
      credentials: [
        {
          key: 'authMethod',
          label: 'Authentication method',
          description: 'How to authenticate.',
          type: 'select',
          options: [
            { label: 'API key', value: 'api_key' },
            { label: 'SSO', value: 'sso' },
          ],
          required: true,
        },
        {
          key: 'apiKey',
          label: 'API key',
          description: 'API token.',
          required: true,
          secret: true,
          when: { key: 'authMethod', op: 'eq', value: 'api_key' },
        },
        {
          key: 'ssoUser',
          label: 'SSO email',
          description: 'Single sign-on email.',
          required: true,
          when: { key: 'authMethod', op: 'eq', value: 'sso' },
        },
      ],
      envSettings: [
        { env: 'AUTH_METHOD', key: 'authMethod' },
        { env: 'API_KEY', key: 'apiKey' },
        { env: 'SSO_USER', key: 'ssoUser' },
      ],
    }
    const ssoSettings: AppSettings = {
      ...BASE_SETTINGS,
      integrationCredentials: {
        'multi-auth': {
          authMethod: 'sso',
          ssoUser: 'alice@example.com',
        },
      },
    }

    const ready = evaluateBuiltInMcp(mcp, ssoSettings)
    assert.equal(ready.status, 'ready')
    if (ready.status !== 'ready') return
    assert.equal(ready.entry.type, 'local')
    if (ready.entry.type !== 'local') return
    assert.equal(ready.entry.environment?.AUTH_METHOD, 'sso')
    assert.equal(ready.entry.environment?.SSO_USER, 'alice@example.com')
    assert.equal(ready.entry.environment?.API_KEY, undefined)

    const missingSso = evaluateBuiltInMcp(mcp, {
      ...BASE_SETTINGS,
      integrationCredentials: {
        'multi-auth': {
          authMethod: 'sso',
        },
      },
    })
    assert.equal(missingSso.status, 'skipped')
    if (missingSso.status !== 'skipped') return
    assert.equal(missingSso.reason, 'not-configured')
  })
})

test('evaluateBuiltInMcp — OAuth MCP is skipped until the user explicitly enables it', () => {
  // Atlassian / Amplitude / etc. — bundled but the user may never
  // intend to use them. Previously showed up as `needs_auth` in the
  // log just from being listed in the downstream config, reading as an
  // error to reviewers scanning the boot log.
  withConfigDir(baseConfig({}), () => {
    const mcp: BundleMcp = {
      name: 'atlassian',
      type: 'remote',
      description: 'Atlassian',
      authMode: 'oauth',
      url: 'https://mcp.atlassian.com/v1/mcp',
    }
    const result = evaluateBuiltInMcp(mcp, BASE_SETTINGS)
    assert.equal(result.status, 'skipped')
    if (result.status !== 'skipped') return
    assert.equal(result.reason, 'awaiting-oauth-opt-in')
  })
})

test('evaluateBuiltInMcp — OAuth MCP is registered once the user toggles integrationEnabled', () => {
  withConfigDir(baseConfig({}), () => {
    const mcp: BundleMcp = {
      name: 'atlassian',
      type: 'remote',
      description: 'Atlassian',
      authMode: 'oauth',
      url: 'https://mcp.atlassian.com/v1/mcp',
    }
    const settings: AppSettings = {
      ...BASE_SETTINGS,
      integrationEnabled: { atlassian: true },
    }
    const result = evaluateBuiltInMcp(mcp, settings)
    assert.equal(result.status, 'ready')
  })
})

test('evaluateBuiltInMcp — explicit disable wins over a credential-complete state', () => {
  // Some users want bundled integrations off even when credentials are
  // present (shared laptops, curiosity-by-default).
  withConfigDir(baseConfig({}), () => {
    const mcp: BundleMcp = {
      name: 'perplexity',
      type: 'local',
      description: 'Perplexity',
      authMode: 'api_token',
      command: ['node', '/tmp/perplexity.js'],
      credentials: [
        { key: 'apiKey', label: 'API key', description: '', required: true, secret: true },
      ],
    }
    const settings: AppSettings = {
      ...BASE_SETTINGS,
      integrationCredentials: { perplexity: { apiKey: 'sk-present' } },
      integrationEnabled: { perplexity: false },
    }
    const result = evaluateBuiltInMcp(mcp, settings)
    assert.equal(result.status, 'skipped')
    if (result.status !== 'skipped') return
    assert.equal(result.reason, 'disabled-by-user')
  })
})

test('evaluateBuiltInMcp — googleAuth MCP without ADC is skipped as not-signed-in-google', () => {
  // The helper points OPEN_COWORK_USER_DATA_DIR at a fresh temp directory,
  // so getAdcPathIfAvailable() returns null even when another test has
  // seeded the default `.open-cowork-test` data dir in parallel.
  withConfigDir(baseConfig({
    auth: {
      mode: 'google-oauth',
      googleOAuth: { clientId: 'test.apps.googleusercontent.com', clientSecret: 's' },
    },
  }), () => {
    const mcp: BundleMcp = {
      name: 'sheets',
      type: 'local',
      description: 'Sheets',
      authMode: 'none',
      command: ['node', '/tmp/sheets.js'],
      googleAuth: true,
    }
    const result = evaluateBuiltInMcp(mcp, BASE_SETTINGS)
    assert.equal(result.status, 'skipped')
    if (result.status !== 'skipped') return
    assert.equal(result.reason, 'not-signed-in-google')
  })
})

test('evaluateBuiltInMcp — credential-less local MCP is ready without any user action', () => {
  // The skills MCP, chart renderer, etc. — bundled infrastructure that
  // every install should have by default.
  withConfigDir(baseConfig({}), () => {
    const mcp: BundleMcp = {
      name: 'skills',
      type: 'local',
      description: 'Custom skills',
      authMode: 'none',
      command: ['node', '/tmp/skills.js'],
    }
    const result = evaluateBuiltInMcp(mcp, BASE_SETTINGS)
    assert.equal(result.status, 'ready')
  })
})
