import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

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

import { loadConfig } from '../config/config-loader.js';

const TMP_BASE = join(tmpdir(), 'robota-cli-test-' + process.pid);

function setupDir(path: string): void {
  mkdirSync(path, { recursive: true });
}

function writeJson(path: string, data: unknown): void {
  writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8');
}

describe('loadConfig', () => {
  let cwd: string;
  let projectDir: string;
  let userDir: string;
  let claudeUserDir: string;
  let claudeProjectDir: string;
  const originalHome = process.env.HOME;

  beforeEach(() => {
    cwd = join(TMP_BASE, 'cwd-' + Math.random().toString(36).slice(2));
    projectDir = join(cwd, '.robota');
    claudeProjectDir = join(cwd, '.claude');
    const homeBase = join(TMP_BASE, 'home-' + Math.random().toString(36).slice(2));
    userDir = join(homeBase, '.robota');
    claudeUserDir = join(homeBase, '.claude');
    setupDir(cwd);
    setupDir(projectDir);
    setupDir(userDir);
    setupDir(claudeUserDir);
    setupDir(claudeProjectDir);
    process.env.HOME = homeBase;
  });

  afterEach(() => {
    process.env.HOME = originalHome;
    if (existsSync(TMP_BASE)) {
      rmSync(TMP_BASE, { recursive: true, force: true });
    }
  });

  it('returns default config when no settings files exist', async () => {
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('moderate');
    expect(config.permissions.allow).toEqual([]);
    expect(config.permissions.deny).toEqual([]);
    expect('memory' in config).toBe(false);
    expect(config.env).toEqual({});
  });

  it('ignores obsolete automatic memory settings', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      memory: {
        policy: 'auto_save',
        retrieval: {
          maxTopics: 5,
          maxTopicChars: 2000,
        },
      },
    });

    const config = await loadConfig(cwd);

    expect('memory' in config).toBe(false);
  });

  it('does not merge obsolete automatic memory settings across layers', async () => {
    writeJson(join(userDir, 'settings.json'), {
      memory: {
        policy: 'disabled',
        retrieval: {
          maxTopics: 2,
          maxTopicChars: 1000,
        },
      },
    });
    writeJson(join(projectDir, 'settings.json'), {
      memory: {
        policy: 'approval_required',
        retrieval: {
          maxTopicChars: 4000,
        },
      },
    });

    const config = await loadConfig(cwd);

    expect('memory' in config).toBe(false);
  });

  it('loads project settings from .robota/settings.json', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      defaultTrustLevel: 'safe',
      currentProvider: 'anthropic',
      providers: { anthropic: { type: 'anthropic', model: 'claude-3-5-sonnet-20241022' } },
    });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('safe');
    expect(config.provider.model).toBe('claude-3-5-sonnet-20241022');
  });

  it('loads auto compact threshold settings', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      autoCompactThreshold: 0.75,
    });

    const config = await loadConfig(cwd);

    expect(config.autoCompactThreshold).toBe(0.75);
  });

  it('loads disabled auto compact settings', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      autoCompactThreshold: false,
    });

    const config = await loadConfig(cwd);

    expect(config.autoCompactThreshold).toBe(false);
  });

  it('loads user settings from ~/.robota/settings.json', async () => {
    writeJson(join(userDir, 'settings.json'), {
      defaultTrustLevel: 'full',
    });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('full');
  });

  it('loads user Claude-compatible settings from ~/.claude/settings.json', async () => {
    writeJson(join(claudeUserDir, 'settings.json'), {
      defaultTrustLevel: 'full',
    });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('full');
  });

  it('project settings take precedence over user settings', async () => {
    writeJson(join(userDir, 'settings.json'), {
      defaultTrustLevel: 'safe',
      currentProvider: 'anthropic',
      providers: { anthropic: { type: 'anthropic', model: 'claude-3-haiku-20240307' } },
    });
    writeJson(join(projectDir, 'settings.json'), {
      defaultTrustLevel: 'moderate',
    });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('moderate');
    // provider profile from user settings is still inherited when not overridden
    expect(config.provider.model).toBe('claude-3-haiku-20240307');
  });

  it('local settings take precedence over project settings', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      defaultTrustLevel: 'safe',
    });
    writeJson(join(projectDir, 'settings.local.json'), {
      defaultTrustLevel: 'full',
    });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('full');
  });

  it('merges permissions arrays (local overrides project overrides user)', async () => {
    writeJson(join(userDir, 'settings.json'), {
      permissions: { allow: ['Bash(git *)'] },
    });
    writeJson(join(projectDir, 'settings.json'), {
      permissions: { allow: ['Read(**)', 'Glob(**)'] },
    });
    const config = await loadConfig(cwd);
    // project overrides user permissions entirely
    expect(config.permissions.allow).toEqual(['Read(**)', 'Glob(**)']);
  });

  it('throws on invalid settings (Zod validation)', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      defaultTrustLevel: 'INVALID_VALUE',
    });
    await expect(loadConfig(cwd)).rejects.toThrow();
  });

  it('throws on invalid auto compact threshold settings', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      autoCompactThreshold: 1.5,
    });

    await expect(loadConfig(cwd)).rejects.toThrow();
  });

  it('resolves $ENV: prefix in apiKey', async () => {
    process.env.TEST_API_KEY_XYZ = 'sk-test-value';
    writeJson(join(projectDir, 'settings.json'), {
      currentProvider: 'anthropic',
      providers: { anthropic: { type: 'anthropic', apiKey: '$ENV:TEST_API_KEY_XYZ' } },
    });
    const config = await loadConfig(cwd);
    expect(config.provider.apiKey).toBe('sk-test-value');
    delete process.env.TEST_API_KEY_XYZ;
  });

  it('resolves active provider profile from currentProvider and providers', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      currentProvider: 'openai',
      providers: {
        openai: {
          type: 'openai',
          model: 'supergemma4-26b-uncensored-v2',
          apiKey: 'lm-studio',
          baseURL: 'http://localhost:1234/v1',
          timeout: 30000,
        },
      },
    });

    const config = await loadConfig(cwd);

    expect(config.provider).toEqual({
      name: 'openai',
      model: 'supergemma4-26b-uncensored-v2',
      apiKey: 'lm-studio',
      baseURL: 'http://localhost:1234/v1',
      timeout: 30000,
    });
  });

  it('preserves provider-owned options in active provider profiles', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      currentProvider: 'qwen',
      providers: {
        qwen: {
          type: 'qwen',
          model: 'qwen3.6-plus',
          apiKey: 'dashscope-key',
          options: {
            builtInWebTools: {
              webSearch: true,
              webFetch: true,
              enableThinking: true,
            },
          },
        },
      },
    });

    const config = await loadConfig(cwd);

    expect(config.provider.options).toEqual({
      builtInWebTools: {
        webSearch: true,
        webFetch: true,
        enableThinking: true,
      },
    });
  });

  it('resolves $ENV: prefix in active provider profile apiKey', async () => {
    process.env.TEST_OPENAI_COMPAT_KEY = 'sk-profile-value';
    writeJson(join(projectDir, 'settings.json'), {
      currentProvider: 'openai',
      providers: {
        openai: {
          type: 'openai',
          model: 'supergemma4-26b-uncensored-v2',
          apiKey: '$ENV:TEST_OPENAI_COMPAT_KEY',
          baseURL: 'http://localhost:1234/v1',
        },
      },
    });

    const config = await loadConfig(cwd);

    expect(config.provider.apiKey).toBe('sk-profile-value');
    delete process.env.TEST_OPENAI_COMPAT_KEY;
  });

  it('ignores removed active provider profile authToken fields', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      currentProvider: 'anthropic',
      providers: {
        anthropic: {
          type: 'anthropic',
          model: 'claude-sonnet-4-6',
          authToken: '$ENV:TEST_ANTHROPIC_AUTH_TOKEN',
        },
      },
    });

    const config = await loadConfig(cwd);

    expect(config.provider).toEqual({
      name: 'anthropic',
      model: 'claude-sonnet-4-6',
      apiKey: undefined,
    });
  });

  it('deep-merges provider profiles across settings layers', async () => {
    writeJson(join(userDir, 'settings.json'), {
      providers: {
        openai: {
          type: 'openai',
          apiKey: 'lm-studio',
        },
      },
    });
    writeJson(join(projectDir, 'settings.json'), {
      currentProvider: 'openai',
      providers: {
        openai: {
          model: 'supergemma4-26b-uncensored-v2',
          baseURL: 'http://localhost:1234/v1',
        },
      },
    });

    const config = await loadConfig(cwd);

    expect(config.provider).toMatchObject({
      name: 'openai',
      model: 'supergemma4-26b-uncensored-v2',
      apiKey: 'lm-studio',
      baseURL: 'http://localhost:1234/v1',
    });
  });

  it('throws when legacy flat provider settings are used without currentProvider', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      provider: {
        name: 'anthropic',
        model: 'claude-sonnet-4-6',
        apiKey: 'sk-ant-test',
      },
      providers: {
        openai: {
          type: 'openai',
          model: 'supergemma4-26b-uncensored-v2',
          apiKey: 'lm-studio',
          baseURL: 'http://localhost:1234/v1',
        },
      },
    });

    await expect(loadConfig(cwd)).rejects.toThrow(
      'Legacy flat "provider" settings are not supported',
    );
  });

  it('throws when currentProvider does not exist in providers', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      currentProvider: 'missing',
      providers: {
        openai: {
          type: 'openai',
          model: 'supergemma4-26b-uncensored-v2',
          apiKey: 'lm-studio',
        },
      },
    });

    await expect(loadConfig(cwd)).rejects.toThrow('currentProvider "missing" was not found');
  });

  // --- .claude/ path support ---

  it('loads settings from .claude/settings.json', async () => {
    writeJson(join(claudeProjectDir, 'settings.json'), {
      defaultTrustLevel: 'full',
    });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('full');
  });

  it('.claude/settings.local.json has highest priority', async () => {
    writeJson(join(claudeProjectDir, 'settings.json'), {
      defaultTrustLevel: 'safe',
    });
    writeJson(join(claudeProjectDir, 'settings.local.json'), {
      defaultTrustLevel: 'full',
    });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('full');
  });

  it('.claude/ paths win over legacy .robota/ paths', async () => {
    writeJson(join(projectDir, 'settings.json'), {
      defaultTrustLevel: 'safe',
      currentProvider: 'anthropic',
      providers: { anthropic: { type: 'anthropic', model: 'robota-model' } },
    });
    writeJson(join(claudeProjectDir, 'settings.json'), {
      defaultTrustLevel: 'full',
    });
    const config = await loadConfig(cwd);
    // .claude/ wins for trust level
    expect(config.defaultTrustLevel).toBe('full');
    // .robota/ provider profile is inherited since .claude/ didn't set it
    expect(config.provider.model).toBe('robota-model');
  });

  it('full 6-file precedence: .claude/settings.local.json wins over all', async () => {
    writeJson(join(userDir, 'settings.json'), { defaultTrustLevel: 'safe' });
    writeJson(join(claudeUserDir, 'settings.json'), { defaultTrustLevel: 'safe' });
    writeJson(join(projectDir, 'settings.json'), { defaultTrustLevel: 'safe' });
    writeJson(join(projectDir, 'settings.local.json'), { defaultTrustLevel: 'safe' });
    writeJson(join(claudeProjectDir, 'settings.json'), { defaultTrustLevel: 'safe' });
    writeJson(join(claudeProjectDir, 'settings.local.json'), { defaultTrustLevel: 'full' });
    const config = await loadConfig(cwd);
    expect(config.defaultTrustLevel).toBe('full');
  });

  it('hooks from .claude/settings.json are loaded and merged', async () => {
    writeJson(join(claudeProjectDir, 'settings.json'), {
      hooks: {
        PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'echo test' }] }],
      },
    });
    const config = await loadConfig(cwd);
    expect(config.hooks).toBeDefined();
    expect(config.hooks?.PreToolUse).toHaveLength(1);
    expect(config.hooks?.PreToolUse?.[0]?.matcher).toBe('Bash');
  });

  it('hooks support all configured lifecycle event types', async () => {
    const allEvents = {
      PreToolUse: [{ matcher: '', hooks: [{ type: 'command', command: 'echo pre' }] }],
      PostToolUse: [{ matcher: '', hooks: [{ type: 'command', command: 'echo post' }] }],
      SessionStart: [{ matcher: '', hooks: [{ type: 'command', command: 'echo start' }] }],
      SessionEnd: [{ matcher: '', hooks: [{ type: 'command', command: 'echo end' }] }],
      Stop: [{ matcher: '', hooks: [{ type: 'command', command: 'echo stop' }] }],
      StopFailure: [{ matcher: '', hooks: [{ type: 'command', command: 'echo stop-fail' }] }],
      PreCompact: [{ matcher: '', hooks: [{ type: 'command', command: 'echo prec' }] }],
      PostCompact: [{ matcher: '', hooks: [{ type: 'command', command: 'echo postc' }] }],
      UserPromptSubmit: [{ matcher: '', hooks: [{ type: 'command', command: 'echo prompt' }] }],
      SubagentStart: [{ matcher: '', hooks: [{ type: 'command', command: 'echo agent-start' }] }],
      SubagentStop: [{ matcher: '', hooks: [{ type: 'command', command: 'echo agent-stop' }] }],
      WorktreeCreate: [{ matcher: '', hooks: [{ type: 'command', command: 'echo wt-create' }] }],
      WorktreeRemove: [{ matcher: '', hooks: [{ type: 'command', command: 'echo wt-remove' }] }],
    };
    writeJson(join(claudeProjectDir, 'settings.json'), { hooks: allEvents });
    const config = await loadConfig(cwd);
    expect(config.hooks).toBeDefined();
    expect(Object.keys(config.hooks ?? {})).toHaveLength(13);
  });

  it('hooks support http hook type', async () => {
    writeJson(join(claudeProjectDir, 'settings.json'), {
      hooks: {
        PreToolUse: [{ matcher: '', hooks: [{ type: 'http', url: 'https://example.com/hook' }] }],
      },
    });
    const config = await loadConfig(cwd);
    expect(config.hooks?.PreToolUse?.[0]?.hooks[0]).toEqual({
      type: 'http',
      url: 'https://example.com/hook',
    });
  });

  // --- enabledPlugins and extraKnownMarketplaces ---

  it('loads enabledPlugins from settings', async () => {
    writeJson(join(claudeProjectDir, 'settings.json'), {
      enabledPlugins: { 'my-plugin': true, 'other-plugin': false },
    });
    const config = await loadConfig(cwd);
    expect(config.enabledPlugins).toEqual({ 'my-plugin': true, 'other-plugin': false });
  });

  it('loads extraKnownMarketplaces from settings', async () => {
    writeJson(join(claudeProjectDir, 'settings.json'), {
      extraKnownMarketplaces: {
        'my-market': { source: { type: 'github', repo: 'org/plugins' } },
      },
    });
    const config = await loadConfig(cwd);
    expect(config.extraKnownMarketplaces).toEqual({
      'my-market': { source: { type: 'github', repo: 'org/plugins' } },
    });
  });

  it('ignores incompatible extraKnownMarketplaces from Claude-compatible settings', async () => {
    writeJson(join(claudeUserDir, 'settings.json'), {
      defaultTrustLevel: 'full',
      extraKnownMarketplaces: {
        'external-market': { source: { repo: 'org/plugins' } },
      },
    });

    const config = await loadConfig(cwd);

    expect(config.defaultTrustLevel).toBe('full');
    expect(config.extraKnownMarketplaces).toBeUndefined();
  });
});
