import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import type { PluginConfig } from '../config';
import {
  getActiveRuntimePreset,
  setActiveRuntimePreset,
} from '../config/runtime-preset';
import { readTuiSnapshot, recordTuiAgentModels } from '../tui-state';
import { createPresetManager } from './preset-manager';

function createMockContext() {
  const configUpdate = mock(async () => ({}));
  return {
    client: {
      config: {
        update: configUpdate,
      },
    },
    directory: '/tmp/test',
  } as any;
}

function createOutput() {
  return { parts: [] as Array<{ type: string; text?: string }> };
}

function getOutputText(output: ReturnType<typeof createOutput>): string {
  return output.parts
    .filter((p) => p.type === 'text')
    .map((p) => p.text ?? '')
    .join('\n');
}

let previousXdgDataHome: string | undefined;
let tempDir: string;

beforeEach(() => {
  previousXdgDataHome = process.env.XDG_DATA_HOME;
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-preset-manager-'));
  process.env.XDG_DATA_HOME = tempDir;
  setActiveRuntimePreset(null);
});

afterEach(() => {
  if (previousXdgDataHome === undefined) {
    delete process.env.XDG_DATA_HOME;
  } else {
    process.env.XDG_DATA_HOME = previousXdgDataHome;
  }

  fs.rmSync(tempDir, { recursive: true, force: true });
  setActiveRuntimePreset(null);
});

describe('createPresetManager', () => {
  describe('handleCommandExecuteBefore', () => {
    test('ignores non-preset commands', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {};
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'auto-continue', sessionID: 's1', arguments: 'on' },
        output,
      );

      expect(output.parts).toHaveLength(0);
      expect(ctx.client.config.update).not.toHaveBeenCalled();
    });

    test('lists available presets when no argument given', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: {
            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
          },
          powerful: {
            orchestrator: { model: 'openai/gpt-5.5' },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: '' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('cheap');
      expect(text).toContain('powerful');
      expect(ctx.client.config.update).not.toHaveBeenCalled();
    });

    test('lists presets with active marker when preset is set', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        preset: 'cheap',
        presets: {
          cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
          powerful: { orchestrator: { model: 'openai/gpt-5.5' } },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: '' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('← active');
    });

    test('shows no-presets message when none configured', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {};
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: '' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('No presets configured');
    });

    test('switches preset and calls config.update', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: {
            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
            explorer: { model: 'openai/gpt-5.4-mini' },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('Switched to preset "cheap"');
      expect(text).toContain('orchestrator');
      expect(text).toContain('anthropic/claude-3.5-haiku');
      expect(text).toContain('explorer');
      expect(ctx.client.config.update).toHaveBeenCalledTimes(1);
      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
            explorer: { model: 'openai/gpt-5.4-mini' },
          },
        },
      });
    });

    test('updates the TUI snapshot after a successful preset switch', async () => {
      recordTuiAgentModels({
        agentModels: {
          explorer: 'openai/gpt-5.4-mini',
          fixer: 'openai/gpt-5.4-mini',
        },
      });

      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: {
            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
            explorer: { model: 'openai/gpt-5.5' },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output,
      );

      expect(readTuiSnapshot().agentModels).toEqual({
        explorer: 'openai/gpt-5.5',
        fixer: 'openai/gpt-5.4-mini',
        orchestrator: 'anthropic/claude-3.5-haiku',
      });
    });

    test('passes temperature in config update', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          precise: {
            orchestrator: { model: 'openai/o3', temperature: 0.1 },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'precise' },
        output,
      );

      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            orchestrator: { model: 'openai/o3', temperature: 0.1 },
          },
        },
      });
    });

    test('passes variant in config update', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          thinker: {
            oracle: {
              model: 'anthropic/claude-sonnet-4-6',
              variant: 'thinking',
            },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'thinker' },
        output,
      );

      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            oracle: {
              model: 'anthropic/claude-sonnet-4-6',
              variant: 'thinking',
            },
          },
        },
      });
    });

    test('shows error for unknown preset name', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'nonexistent' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('not found');
      expect(text).toContain('cheap');
      expect(ctx.client.config.update).not.toHaveBeenCalled();
    });

    test('shows error when no presets configured but argument given', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {};
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('not found');
      expect(text).toContain('No presets configured');
    });

    test('handles config.update error gracefully', async () => {
      recordTuiAgentModels({
        agentModels: {
          explorer: 'openai/gpt-5.4-mini',
        },
      });

      const ctx = createMockContext();
      ctx.client.config.update = mock(async () => {
        throw new Error('Server unavailable');
      });
      const config: PluginConfig = {
        presets: {
          cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('Failed to switch preset');
      expect(text).toContain('Server unavailable');
      expect(readTuiSnapshot().agentModels).toEqual({
        explorer: 'openai/gpt-5.4-mini',
      });
    });

    test('shows empty preset message when preset has no valid overrides', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          empty: {
            orchestrator: {},
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'empty' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('empty');
      expect(ctx.client.config.update).not.toHaveBeenCalled();
    });

    test('forwards options field in config update', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          thinker: {
            oracle: {
              model: 'anthropic/claude-sonnet-4-6',
              options: {
                thinking: { type: 'enabled', budgetTokens: 10000 },
              },
            },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'thinker' },
        output,
      );

      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            oracle: {
              model: 'anthropic/claude-sonnet-4-6',
              options: {
                thinking: { type: 'enabled', budgetTokens: 10000 },
              },
            },
          },
        },
      });
    });

    test('trims whitespace from preset name argument', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: '  cheap  ' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('Switched to preset "cheap"');
      expect(ctx.client.config.update).toHaveBeenCalledTimes(1);
    });

    test('shows suggestion for multi-word arguments', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap powerful' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('cannot contain spaces');
      expect(text).toContain('/preset cheap');
      expect(ctx.client.config.update).not.toHaveBeenCalled();
    });

    test('catches tab-separated arguments', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap\tpowerful' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('cannot contain spaces');
      expect(ctx.client.config.update).not.toHaveBeenCalled();
    });

    test('skips agents with empty overrides in mixed preset', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          mixed: {
            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
            explorer: {},
            oracle: { temperature: 0.3 },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'mixed' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('Switched to preset "mixed"');
      // Only orchestrator and oracle should be forwarded
      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
            oracle: { temperature: 0.3 },
          },
        },
      });
    });

    test('resolves array-form model to first entry', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          fallback: {
            orchestrator: {
              model: ['anthropic/claude-3.5-haiku', 'openai/gpt-5.5'],
            },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'fallback' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('Switched to preset "fallback"');
      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
          },
        },
      });
    });

    test('resolves array-form model with object entries', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          thinker: {
            oracle: {
              model: [
                { id: 'anthropic/claude-sonnet-4-6', variant: 'thinking' },
                { id: 'openai/o3' },
              ],
            },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'thinker' },
        output,
      );

      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            oracle: {
              model: 'anthropic/claude-sonnet-4-6',
              variant: 'thinking',
            },
          },
        },
      });
    });

    test('shows variant and options in switch summary', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          thinker: {
            oracle: {
              model: 'anthropic/claude-sonnet-4-6',
              variant: 'thinking',
              options: { thinking: { type: 'enabled', budgetTokens: 10000 } },
            },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output = createOutput();

      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'thinker' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('variant: thinking');
      expect(text).toContain('options: yes');
    });

    test('tracks active preset after switch', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: { orchestrator: { model: 'anthropic/claude-3.5-haiku' } },
          powerful: { orchestrator: { model: 'openai/gpt-5.5' } },
        },
      };
      const manager = createPresetManager(ctx, config);

      // Switch to cheap
      const output1 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output1,
      );
      expect(getOutputText(output1)).toContain('Switched');

      // List presets should now show cheap as active
      const output2 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: '' },
        output2,
      );
      expect(getOutputText(output2)).toContain('cheap ← active');

      // Switch to powerful
      const output3 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'powerful' },
        output3,
      );
      expect(getOutputText(output3)).toContain('Switched to preset "powerful"');

      // List should now show powerful as active
      const output4 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: '' },
        output4,
      );
      expect(getOutputText(output4)).toContain('powerful ← active');

      // Cleanup module state
      setActiveRuntimePreset(null);
    });
  });

  describe('registerCommand', () => {
    test('registers preset command when not present', () => {
      const ctx = createMockContext();
      const config: PluginConfig = {};
      const manager = createPresetManager(ctx, config);
      const opencodeConfig: Record<string, unknown> = {};

      manager.registerCommand(opencodeConfig);

      const command = (opencodeConfig.command as Record<string, unknown>)
        .preset as { template: string; description: string };
      expect(command).toBeDefined();
      expect(command.template).toContain('presets');
      expect(command.description).toContain('/preset');
    });

    test('does not overwrite existing preset command', () => {
      const ctx = createMockContext();
      const config: PluginConfig = {};
      const manager = createPresetManager(ctx, config);
      const existing = { template: 'custom', description: 'custom' };
      const opencodeConfig: Record<string, unknown> = {
        command: { preset: existing },
      };

      manager.registerCommand(opencodeConfig);

      expect((opencodeConfig.command as Record<string, unknown>).preset).toBe(
        existing,
      );
    });
  });

  describe('preset switching stale state', () => {
    test('reset updates for agents removed when switching presets', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: {
            oracle: { model: 'cheap-model', temperature: 0.3 },
          },
          powerful: {
            orchestrator: { model: 'powerful-model' },
          },
        },
        agents: {
          oracle: { model: 'baseline-model' },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output1 = createOutput();

      // Switch to cheap first
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output1,
      );
      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            oracle: { model: 'cheap-model', temperature: 0.3 },
          },
        },
      });

      // Reset mock for next call
      ctx.client.config.update.mockClear();

      const output2 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'powerful' },
        output2,
      );

      // Second update should reset oracle to baseline and set orchestrator
      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            oracle: { model: 'baseline-model' },
            orchestrator: { model: 'powerful-model' },
          },
        },
      });

      // Cleanup
      setActiveRuntimePreset(null);
    });

    test('no reset updates when new preset covers same agents', async () => {
      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: {
            oracle: { model: 'a' },
          },
          cheaper: {
            oracle: { model: 'b' },
          },
        },
      };
      const manager = createPresetManager(ctx, config);
      const output1 = createOutput();

      // Switch to cheap first
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output1,
      );
      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            oracle: { model: 'a' },
          },
        },
      });

      // Reset mock for next call
      ctx.client.config.update.mockClear();

      const output2 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheaper' },
        output2,
      );

      // Second update should only have oracle, no reset updates
      expect(ctx.client.config.update).toHaveBeenCalledWith({
        body: {
          agent: {
            oracle: { model: 'b' },
          },
        },
      });

      // Cleanup
      setActiveRuntimePreset(null);
    });

    test('preset state rolled back on config.update error', async () => {
      const ctx = createMockContext();
      ctx.client.config.update = mock(async () => {
        throw new Error('Server unavailable');
      });
      const config: PluginConfig = {
        presets: {
          cheap: {
            oracle: { model: 'a' },
          },
          expensive: {
            oracle: { model: 'b' },
          },
        },
      };
      const manager = createPresetManager(ctx, config);

      // Reset mock for successful switch
      ctx.client.config.update = mock(async () => ({}));

      // Switch to cheap successfully
      const output1 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
        output1,
      );
      expect(getActiveRuntimePreset()).toBe('cheap');

      // Reset mock to throw error
      ctx.client.config.update = mock(async () => {
        throw new Error('Server unavailable');
      });

      // Try to switch to expensive but it fails
      const output2 = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: 'expensive' },
        output2,
      );

      // Active preset should still be "cheap" after error
      expect(getActiveRuntimePreset()).toBe('cheap');
      expect(getOutputText(output2)).toContain('Failed to switch preset');

      // Cleanup
      setActiveRuntimePreset(null);
    });

    test('activePreset syncs from runtime-preset state on factory creation', async () => {
      // Set runtime preset before creating manager
      setActiveRuntimePreset('cheap');

      const ctx = createMockContext();
      const config: PluginConfig = {
        presets: {
          cheap: {
            oracle: { model: 'a' },
          },
          powerful: {
            oracle: { model: 'b' },
          },
        },
      };

      // Create manager - should sync from module-level state
      const manager = createPresetManager(ctx, config);

      // List presets should show cheap as active
      const output = createOutput();
      await manager.handleCommandExecuteBefore(
        { command: 'preset', sessionID: 's1', arguments: '' },
        output,
      );

      const text = getOutputText(output);
      expect(text).toContain('cheap ← active');
      expect(text).toContain('powerful');

      // Cleanup
      setActiveRuntimePreset(null);
    });
  });
});
