// tests/kb-update/test-auth-mode.test.mjs
// Unit tests for scripts/kb-update/lib/auth-mode.mjs
// Note: Test fixture credential values are deliberately short (<8 chars) to
// stay below the secrets-scanner heuristic. They are stub markers, not keys.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
  detectAuthMode,
  validateAuthForCron,
  readClaudeJson,
} from '../../scripts/kb-update/lib/auth-mode.mjs';

function withTmp(fn) {
  const dir = mkdtempSync(join(tmpdir(), 'auth-test-'));
  try {
    return fn(dir);
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
}

function makeStubRunner(exitCode) {
  const calls = [];
  const runner = (cmd, args) => {
    calls.push({ cmd, args });
    return exitCode;
  };
  return { runner, calls };
}

const MISSING_PATH = '/__definitely__/__not__/__a__/__path__/.claude.json';

test('detectAuthMode — ANTHROPIC_API_KEY set → api-key', () => {
  const { runner } = makeStubRunner(0);
  const mode = detectAuthMode({
    env: { ANTHROPIC_API_KEY: 'fake' },
    runner,
    claudeJsonPath: MISSING_PATH,
  });
  assert.equal(mode, 'api-key');
});

test('detectAuthMode — empty ANTHROPIC_API_KEY is ignored', () => {
  const { runner } = makeStubRunner(1);
  const mode = detectAuthMode({
    env: { ANTHROPIC_API_KEY: '   ' },
    runner,
    claudeJsonPath: MISSING_PATH,
  });
  assert.equal(mode, 'unauthenticated');
});

test('detectAuthMode — CLAUDE_CODE_OAUTH set → long-oauth', () => {
  const { runner } = makeStubRunner(0);
  const mode = detectAuthMode({
    env: { CLAUDE_CODE_OAUTH_TOKEN: 'oat' },
    runner,
    claudeJsonPath: MISSING_PATH,
  });
  assert.equal(mode, 'long-oauth');
});

test('detectAuthMode — both env vars set → api-key precedence', () => {
  const { runner } = makeStubRunner(0);
  const mode = detectAuthMode({
    env: {
      ANTHROPIC_API_KEY: 'fake',
      CLAUDE_CODE_OAUTH_TOKEN: 'oat',
    },
    runner,
    claudeJsonPath: MISSING_PATH,
  });
  assert.equal(mode, 'api-key');
});

test('detectAuthMode — neither env, no claude.json → unauthenticated', () => {
  const { runner, calls } = makeStubRunner(0);
  const mode = detectAuthMode({
    env: {},
    runner,
    claudeJsonPath: MISSING_PATH,
  });
  assert.equal(mode, 'unauthenticated');
  // Runner must NOT be invoked when claude.json is unreadable.
  assert.equal(calls.length, 0);
});

test('detectAuthMode — claude.json onboarded + runner exit 0 → subscription-browser-only', () => {
  withTmp((tmp) => {
    const path = join(tmp, '.claude.json');
    writeFileSync(
      path,
      JSON.stringify({ hasCompletedOnboarding: true, userID: 'abc' }),
      'utf8'
    );
    const { runner, calls } = makeStubRunner(0);
    const mode = detectAuthMode({ env: {}, runner, claudeJsonPath: path });
    assert.equal(mode, 'subscription-browser-only');
    assert.deepEqual(calls, [{ cmd: 'claude', args: ['auth', 'status'] }]);
  });
});

test('detectAuthMode — claude.json onboarded + runner exit 1 → unauthenticated', () => {
  withTmp((tmp) => {
    const path = join(tmp, '.claude.json');
    writeFileSync(
      path,
      JSON.stringify({ hasCompletedOnboarding: true }),
      'utf8'
    );
    const { runner } = makeStubRunner(1);
    const mode = detectAuthMode({ env: {}, runner, claudeJsonPath: path });
    assert.equal(mode, 'unauthenticated');
  });
});

test('detectAuthMode — claude.json present but not onboarded → unauthenticated', () => {
  withTmp((tmp) => {
    const path = join(tmp, '.claude.json');
    writeFileSync(
      path,
      JSON.stringify({ hasCompletedOnboarding: false }),
      'utf8'
    );
    const { runner, calls } = makeStubRunner(0);
    const mode = detectAuthMode({ env: {}, runner, claudeJsonPath: path });
    assert.equal(mode, 'unauthenticated');
    assert.equal(calls.length, 0);
  });
});

test('readClaudeJson — returns parsed object on valid JSON', () => {
  withTmp((tmp) => {
    const path = join(tmp, '.claude.json');
    writeFileSync(path, '{"hasCompletedOnboarding": true, "x": 42}', 'utf8');
    const obj = readClaudeJson(path);
    assert.deepEqual(obj, { hasCompletedOnboarding: true, x: 42 });
  });
});

test('readClaudeJson — returns null on missing file', () => {
  assert.equal(readClaudeJson(MISSING_PATH), null);
});

test('readClaudeJson — returns null on malformed JSON', () => {
  withTmp((tmp) => {
    const path = join(tmp, 'bad.json');
    writeFileSync(path, 'not json {', 'utf8');
    assert.equal(readClaudeJson(path), null);
  });
});

test('validateAuthForCron — api-key passes silently', () => {
  validateAuthForCron('api-key');
});

test('validateAuthForCron — long-oauth passes silently', () => {
  validateAuthForCron('long-oauth');
});

test('validateAuthForCron — subscription-browser-only throws EAUTHCRON', () => {
  assert.throws(
    () => validateAuthForCron('subscription-browser-only'),
    (err) =>
      err.code === 'EAUTHCRON' &&
      err.detectedMode === 'subscription-browser-only' &&
      /claude setup-token/.test(err.message) &&
      /ANTHROPIC_API_KEY/.test(err.message)
  );
});

test('validateAuthForCron — unauthenticated throws EAUTHCRON', () => {
  assert.throws(
    () => validateAuthForCron('unauthenticated'),
    (err) => err.code === 'EAUTHCRON' && err.detectedMode === 'unauthenticated'
  );
});
