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

import { createDefaultCommandModules } from '@robota-sdk/agent-command';
import {
  CommandRegistry,
  InteractiveSession,
  readMergedProviderSettings,
  readSettings,
  resolveProviderSettingsWriteTargetPath,
  writeSettings,
} from '@robota-sdk/agent-framework';
import { createHeadlessTransport } from '@robota-sdk/agent-transport/headless';
import { describe, expect, it, vi } from 'vitest';

import { startCli } from '../cli.js';

import type {
  IAIProvider,
  IChatOptions,
  IProviderRequest,
  IRawProviderResponse,
  TUniversalMessage,
} from '@robota-sdk/agent-core';
import type {
  IProviderCommandSettingsAdapter,
  TProviderSettingsDocument,
} from '@robota-sdk/agent-framework';

const createProviderSettingsAdapter = (cwd: string): IProviderCommandSettingsAdapter => ({
  readMergedSettings: () => readMergedProviderSettings(cwd),
  readTargetSettings: () =>
    readSettings(resolveProviderSettingsWriteTargetPath(cwd)) as TProviderSettingsDocument,
  writeTargetSettings: (settings: TProviderSettingsDocument) =>
    writeSettings(resolveProviderSettingsWriteTargetPath(cwd), settings),
});

const noopProviderSettingsAdapter = {
  readMergedSettings: () => ({}) as TProviderSettingsDocument,
  readTargetSettings: () => ({}) as TProviderSettingsDocument,
  writeTargetSettings: () => undefined,
};

function createFakeProvider(): IAIProvider {
  return {
    name: 'fake',
    version: 'test',
    async chat(
      _messages: TUniversalMessage[],
      _options?: IChatOptions,
    ): Promise<TUniversalMessage> {
      return {
        id: 'assistant-1',
        role: 'assistant',
        content: 'unused',
        timestamp: new Date(),
        state: 'complete',
      };
    },
    async generateResponse(_payload: IProviderRequest): Promise<IRawProviderResponse> {
      return { content: 'unused' };
    },
    supportsTools: () => false,
    validateConfig: () => true,
  };
}

function parseJsonObject(output: string): Record<string, unknown> {
  const parsed = JSON.parse(output) as unknown;
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
    throw new Error('Expected JSON object output');
  }
  return parsed as Record<string, unknown>;
}

describe('default CLI command composition', () => {
  it('exposes permissions mode subcommands and standalone mode command', () => {
    const registry = new CommandRegistry();

    for (const module of createDefaultCommandModules({
      cwd: '/workspace',
      providerDefinitions: [],
      providerSettingsAdapter: noopProviderSettingsAdapter,
    })) {
      registry.addModule(module);
    }

    expect(registry.getCommands().map((command) => command.name)).toContain('mode');
    expect(registry.getCommands().map((command) => command.name)).toContain('user-local');
    expect(registry.getSubcommands('permissions').map((command) => command.name)).toEqual([
      'plan',
      'default',
      'acceptEdits',
      'bypassPermissions',
    ]);
  });

  it('routes direct user-local storage inspection before provider setup', async () => {
    const cwd = mkdtempSync(join(tmpdir(), 'robota-direct-user-local-'));
    const home = mkdtempSync(join(tmpdir(), 'robota-direct-user-local-home-'));
    const originalArgv = process.argv;
    const originalHome = process.env.HOME;
    const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(cwd);
    const writes: string[] = [];
    const originalWrite = process.stdout.write;
    process.argv = ['node', 'robota', 'user-local', 'storage', 'list', '--format', 'json'];
    process.env.HOME = home;
    process.stdout.write = ((chunk: string) => {
      writes.push(chunk);
      return true;
    }) as typeof process.stdout.write;

    try {
      await startCli({ providerDefinitions: [] });
      const output = parseJsonObject(writes.join('').trim());
      expect(output['root']).toBe(join(home, '.robota'));
      const categories = output['categories'];
      expect(Array.isArray(categories)).toBe(true);
      if (!Array.isArray(categories)) {
        throw new Error('Expected categories array');
      }
      const categoryNames = categories.map((item) => {
        if (typeof item !== 'object' || item === null || Array.isArray(item)) {
          throw new Error('Expected category object');
        }
        const category = (item as Record<string, unknown>)['category'];
        if (typeof category !== 'string') {
          throw new Error('Expected category string');
        }
        return category;
      });
      expect(categoryNames).toEqual([
        'preferences',
        'view-state',
        'memory-projections',
        'task-associations',
        'workflow-metadata',
        'inspection-index',
      ]);
      expect(existsSync(join(cwd, '.robota'))).toBe(false);
    } finally {
      process.stdout.write = originalWrite;
      process.argv = originalArgv;
      cwdSpy.mockRestore();
      if (originalHome === undefined) {
        delete process.env.HOME;
      } else {
        process.env.HOME = originalHome;
      }
      rmSync(cwd, { recursive: true, force: true });
      rmSync(home, { recursive: true, force: true });
    }
  });

  it('routes direct user-local memory commands before provider setup', async () => {
    const cwd = mkdtempSync(join(tmpdir(), 'robota-direct-user-local-memory-'));
    const home = mkdtempSync(join(tmpdir(), 'robota-direct-user-local-memory-home-'));
    const originalArgv = process.argv;
    const originalHome = process.env.HOME;
    const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(cwd);
    const writes: string[] = [];
    const originalWrite = process.stdout.write;
    process.argv = [
      'node',
      'robota',
      'user-local',
      'memory',
      'set',
      'view-preference',
      'last-panel',
      'background',
      '--summary',
      'Open the background panel',
      '--source',
      'user-input',
    ];
    process.env.HOME = home;
    process.stdout.write = ((chunk: string) => {
      writes.push(chunk);
      return true;
    }) as typeof process.stdout.write;

    try {
      await startCli({ providerDefinitions: [] });
      expect(writes.join('')).toContain('Stored user-local memory item view-preference/last-panel');
      expect(existsSync(join(cwd, '.robota'))).toBe(false);
    } finally {
      process.stdout.write = originalWrite;
      process.argv = originalArgv;
      cwdSpy.mockRestore();
      if (originalHome === undefined) {
        delete process.env.HOME;
      } else {
        process.env.HOME = originalHome;
      }
      rmSync(cwd, { recursive: true, force: true });
      rmSync(home, { recursive: true, force: true });
    }
  });

  it('runs permissions mode changes through headless slash-command execution', async () => {
    const cwd = mkdtempSync(join(tmpdir(), 'robota-headless-permissions-'));
    const session = new InteractiveSession({
      cwd,
      provider: createFakeProvider(),
      config: {
        defaultTrustLevel: 'moderate',
        language: 'en',
        provider: {
          name: 'fake',
          model: 'fake-model',
          apiKey: 'test-key',
        },
        permissions: { allow: [], deny: [] },
        env: {},
      },
      bare: true,
      permissionMode: 'default',
      commandModules: createDefaultCommandModules({
        cwd,
        providerDefinitions: [],
        providerSettingsAdapter: noopProviderSettingsAdapter,
      }),
    });
    const writes: string[] = [];
    const originalWrite = process.stdout.write;
    process.stdout.write = ((chunk: string) => {
      writes.push(chunk);
      return true;
    }) as typeof process.stdout.write;

    try {
      const transport = createHeadlessTransport({
        outputFormat: 'json',
        prompt: '/permissions plan',
      });

      session.attachTransport(transport);
      await transport.start();

      expect(transport.getExitCode()).toBe(0);
      expect(session.getSession().getPermissionMode()).toBe('plan');
      expect(parseJsonObject(writes.join('').trim())).toMatchObject({
        type: 'result',
        result: 'Permission mode set to: plan\nPermission mode: plan\nNo session-approved tools.',
        subtype: 'success',
      });
    } finally {
      process.stdout.write = originalWrite;
      await session.shutdown({ reason: 'prompt_input_exit' });
      rmSync(cwd, { recursive: true, force: true });
    }
  });

  it('prints provider profile lists in headless mode without blocking on TUI interactions', async () => {
    const cwd = mkdtempSync(join(tmpdir(), 'robota-headless-provider-'));
    mkdirSync(join(cwd, '.robota'), { recursive: true });
    writeFileSync(
      join(cwd, '.robota', 'settings.local.json'),
      JSON.stringify({
        currentProvider: 'openai',
        providers: {
          openai: { type: 'openai', model: 'gpt-4o', apiKey: 'sk-openai' },
          anthropic: {
            type: 'anthropic',
            model: 'claude-sonnet-4-6',
            apiKey: '$ENV:ANTHROPIC_API_KEY',
          },
        },
      }),
    );
    const session = new InteractiveSession({
      cwd,
      provider: createFakeProvider(),
      config: {
        defaultTrustLevel: 'moderate',
        language: 'en',
        provider: {
          name: 'fake',
          model: 'fake-model',
          apiKey: 'test-key',
        },
        permissions: { allow: [], deny: [] },
        env: {},
      },
      bare: true,
      permissionMode: 'default',
      commandModules: createDefaultCommandModules({
        cwd,
        providerDefinitions: [],
        providerSettingsAdapter: createProviderSettingsAdapter(cwd),
      }),
    });
    const writes: string[] = [];
    const originalWrite = process.stdout.write;
    process.stdout.write = ((chunk: string) => {
      writes.push(chunk);
      return true;
    }) as typeof process.stdout.write;

    try {
      const transport = createHeadlessTransport({
        outputFormat: 'json',
        prompt: '/provider',
      });

      session.attachTransport(transport);
      await transport.start();

      expect(transport.getExitCode()).toBe(0);
      const output = parseJsonObject(writes.join('').trim());
      expect(output).toMatchObject({
        type: 'result',
        subtype: 'success',
      });
      expect(String(output['result'])).toContain('* openai: openai gpt-4o');
      expect(String(output['result'])).toContain('- anthropic: anthropic claude-sonnet-4-6');
    } finally {
      process.stdout.write = originalWrite;
      await session.shutdown({ reason: 'prompt_input_exit' });
      rmSync(cwd, { recursive: true, force: true });
    }
  });
});
