import fs from 'fs';
import path from 'path';

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

describe('config/env loading', () => {
  let tempRoot: string;
  let previousCwd: string;
  let previousEnv: NodeJS.ProcessEnv;

  beforeEach(() => {
    tempRoot = fs.mkdtempSync(path.join('/tmp', 'ejclaw-config-'));
    previousCwd = process.cwd();
    previousEnv = { ...process.env };
    process.chdir(tempRoot);
    delete process.env.EJCLAW_STORE_DIR;
    delete process.env.EJCLAW_GROUPS_DIR;
    delete process.env.EJCLAW_DATA_DIR;
    delete process.env.EJCLAW_CACHE_DIR;
    delete process.env.AGENT_TIMEOUT;
    delete process.env.LOG_LEVEL;
    delete process.env.CLAUDE_MODEL;
    delete process.env.CODEX_MODEL;
    delete process.env.STATUS_CHANNEL_ID;
    delete process.env.TZ;
    delete process.env.DISCORD_BOT_TOKEN;
    delete process.env.DISCORD_CLAUDE_BOT_TOKEN;
    delete process.env.DISCORD_CODEX_BOT_TOKEN;
    delete process.env.DISCORD_CODEX_MAIN_BOT_TOKEN;
    delete process.env.DISCORD_REVIEW_BOT_TOKEN;
    delete process.env.DISCORD_CODEX_REVIEW_BOT_TOKEN;
    delete process.env.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL;
    delete process.env.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION;
    delete process.env.CODEX_WARMUP_ENABLED;
    delete process.env.CODEX_WARMUP_PROMPT;
    delete process.env.CODEX_WARMUP_MODEL;
    delete process.env.CODEX_WARMUP_INTERVAL_MS;
    delete process.env.CODEX_WARMUP_MIN_INTERVAL_MS;
    delete process.env.CODEX_WARMUP_STAGGER_MS;
    delete process.env.CODEX_WARMUP_MAX_USAGE_PCT;
    delete process.env.CODEX_WARMUP_MAX_D7_USAGE_PCT;
    delete process.env.CODEX_WARMUP_COMMAND_TIMEOUT_MS;
    delete process.env.CODEX_WARMUP_FAILURE_COOLDOWN_MS;
    delete process.env.CODEX_WARMUP_MAX_CONSECUTIVE_FAILURES;
    delete process.env.WEB_DASHBOARD_ENABLED;
    delete process.env.WEB_DASHBOARD_HOST;
    delete process.env.WEB_DASHBOARD_PORT;
    delete process.env.WEB_DASHBOARD_STATIC_DIR;
    delete process.env.WEB_DASHBOARD_TOKEN;
    delete process.env.SESSION_COMMAND_USER_IDS;
    vi.resetModules();
  });

  afterEach(() => {
    process.chdir(previousCwd);
    for (const key of Object.keys(process.env)) {
      if (!(key in previousEnv)) delete process.env[key];
    }
    for (const [key, value] of Object.entries(previousEnv)) {
      if (value === undefined) {
        delete process.env[key];
      } else {
        process.env[key] = value;
      }
    }
    fs.rmSync(tempRoot, { recursive: true, force: true });
  });

  it('preserves empty-string env values and still prefers process.env over .env', async () => {
    fs.writeFileSync(
      path.join(tempRoot, '.env'),
      [
        'FROM_FILE=file-value',
        'EMPTY_IN_FILE=',
        'PROCESS_EMPTY=file-fallback',
      ].join('\n'),
    );
    process.env.FROM_FILE = 'process-value';
    process.env.PROCESS_EMPTY = '';

    const env = await import('./env.js');

    expect(env.getEnv('FROM_FILE')).toBe('process-value');
    expect(env.getEnv('EMPTY_IN_FILE')).toBe('');
    expect(env.getEnv('PROCESS_EMPTY')).toBe('');
    expect(env.readEnvFile(['EMPTY_IN_FILE', 'PROCESS_EMPTY'])).toEqual({
      EMPTY_IN_FILE: '',
      PROCESS_EMPTY: 'file-fallback',
    });
  });

  it('loads formerly process-only config values through the shared env path', async () => {
    fs.writeFileSync(
      path.join(tempRoot, '.env'),
      [
        'EJCLAW_STORE_DIR=./custom-store',
        'EJCLAW_GROUPS_DIR=./custom-groups',
        'EJCLAW_DATA_DIR=./custom-data',
        'EJCLAW_CACHE_DIR=./custom-cache',
        'AGENT_TIMEOUT=12345',
        'LOG_LEVEL=trace',
        'CLAUDE_MODEL=claude-opus-test',
        'CODEX_MODEL=gpt-5.4-test',
        'STATUS_CHANNEL_ID=status-room',
        'TZ=UTC',
      ].join('\n'),
    );

    const config = await import('./config.js');

    expect(config.STORE_DIR).toBe(path.resolve(tempRoot, 'custom-store'));
    expect(config.GROUPS_DIR).toBe(path.resolve(tempRoot, 'custom-groups'));
    expect(config.DATA_DIR).toBe(path.resolve(tempRoot, 'custom-data'));
    expect(config.CACHE_DIR).toBe(path.resolve(tempRoot, 'custom-cache'));
    expect(config.AGENT_TIMEOUT).toBe(12345);
    expect(config.LOG_LEVEL).toBe('trace');
    expect(config.DEFAULT_CLAUDE_MODEL).toBe('claude-opus-test');
    expect(config.DEFAULT_CODEX_MODEL).toBe('gpt-5.4-test');
    expect(config.STATUS_CHANNEL_ID).toBe('status-room');
    expect(config.TIMEZONE).toBe('UTC');
  });

  it('defaults latest-owner-final carry-forward to disabled and honors explicit opt-in', async () => {
    let config = await import('./config.js');
    expect(config.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL).toBe(false);

    vi.resetModules();
    process.env.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL = 'true';
    config = await import('./config.js');
    expect(config.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL).toBe(true);
  });

  it('defaults unsafe-host Claude reviewer fresh-session forcing to disabled and honors explicit opt-in', async () => {
    let config = await import('./config.js');
    expect(config.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION).toBe(false);

    vi.resetModules();
    process.env.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION = 'true';
    config = await import('./config.js');
    expect(config.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION).toBe(true);
  });

  it('keeps Codex warm-up disabled by default and exposes conservative opt-in env config', async () => {
    let config = await import('./config.js');
    expect(config.CODEX_WARMUP_CONFIG.enabled).toBe(false);
    expect(config.CODEX_WARMUP_CONFIG.maxUsagePct).toBe(0);
    expect(config.CODEX_WARMUP_CONFIG.maxD7UsagePct).toBe(0);
    expect(
      config.CODEX_WARMUP_CONFIG.maxConsecutiveFailures,
    ).toBeGreaterThanOrEqual(1);

    vi.resetModules();
    process.env.CODEX_MODEL = 'gpt-5.5';
    process.env.CODEX_WARMUP_ENABLED = 'true';
    process.env.CODEX_WARMUP_PROMPT = '.';
    process.env.CODEX_WARMUP_STAGGER_MS = '600000';
    process.env.CODEX_WARMUP_MAX_CONSECUTIVE_FAILURES = '1';
    config = await import('./config.js');

    expect(config.CODEX_WARMUP_CONFIG).toEqual(
      expect.objectContaining({
        enabled: true,
        prompt: '.',
        model: 'gpt-5.5',
        staggerMs: 600000,
        maxConsecutiveFailures: 1,
      }),
    );
  });

  it('keeps the web dashboard disabled by default and loads explicit bind/static settings', async () => {
    let config = await import('./config.js');
    expect(config.WEB_DASHBOARD.enabled).toBe(false);
    expect(config.WEB_DASHBOARD.host).toBe('127.0.0.1');
    expect(config.WEB_DASHBOARD.port).toBe(8734);
    expect(config.WEB_DASHBOARD.staticDir).toBe(
      path.resolve(tempRoot, 'apps', 'dashboard', 'dist'),
    );
    expect(config.WEB_DASHBOARD.token).toBe('');

    vi.resetModules();
    process.env.WEB_DASHBOARD_ENABLED = 'true';
    process.env.WEB_DASHBOARD_HOST = '0.0.0.0';
    process.env.WEB_DASHBOARD_PORT = '9001';
    process.env.WEB_DASHBOARD_STATIC_DIR = './custom-dashboard-dist';
    process.env.WEB_DASHBOARD_TOKEN = 'mobile-secret';
    config = await import('./config.js');

    expect(config.WEB_DASHBOARD).toEqual({
      enabled: true,
      host: '0.0.0.0',
      port: 9001,
      staticDir: path.resolve(tempRoot, 'custom-dashboard-dist'),
      token: 'mobile-secret',
    });
  });

  it('fails fast when a legacy Discord token alias is configured', async () => {
    process.env.DISCORD_BOT_TOKEN = 'legacy...oken';

    const { loadConfig } = await import('./config/load-config.js');

    expect(() => loadConfig()).toThrow(
      /Legacy env aliases are no longer supported; remove or rename \(DISCORD_BOT_TOKEN\) to the canonical keys/,
    );
  });

  it('fails fast when SESSION_COMMAND_USER_IDS is configured', async () => {
    process.env.SESSION_COMMAND_USER_IDS = 'alice,bob';

    const { loadConfig } = await import('./config/load-config.js');

    expect(() => loadConfig()).toThrow(
      /Legacy env aliases are no longer supported; remove or rename \(SESSION_COMMAND_USER_IDS\) to the canonical keys/,
    );
  });
});
