import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createConfigCommand } from '../../src/commands/config.js';
import type { ApiClient } from '../../src/api-client.js';
import { saveCredentials, loadCredentials } from '../../src/auth/index.js';

function mockClient(): ApiClient {
  return {
    get: vi.fn(async () => ({})),
    post: vi.fn(async () => ({ token: 'impersonated-tok', user: { email: 'other@test.com' } })),
    put: vi.fn(async () => ({})),
    delete: vi.fn(async () => {}),
  } as unknown as ApiClient;
}

describe('config claude', () => {
  let client: ReturnType<typeof mockClient>;
  let output: string[];
  let tmpDir: string;
  const log = (...args: string[]) => output.push(args.join(' '));

  beforeEach(() => {
    client = mockClient();
    output = [];
    tmpDir = mkdtempSync(join(tmpdir(), 'mcpctl-config-claude-'));
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('generates .mcp.json with mcpctl mcp bridge entry', async () => {
    const outPath = join(tmpDir, '.mcp.json');
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    // PR-5: --skip-skills bypasses the new sync + SessionStart hook side
    // effects so this test stays focused on .mcp.json generation. The new
    // sync flow has its own tests under src/cli/tests/utils/.
    await cmd.parseAsync(['claude', '--project', 'homeautomation', '-o', outPath, '--skip-skills'], { from: 'user' });

    // No API call should be made when --skip-skills is set.
    expect(client.get).not.toHaveBeenCalled();

    const written = JSON.parse(readFileSync(outPath, 'utf-8'));
    expect(written.mcpServers['homeautomation']).toEqual({
      command: 'mcpctl',
      args: ['mcp', '-p', 'homeautomation'],
    });
    expect(output.join('\n')).toContain('1 server(s)');
  });

  it('prints to stdout with --stdout', async () => {
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['claude', '--project', 'myproj', '--stdout'], { from: 'user' });

    const parsed = JSON.parse(output[0]);
    expect(parsed.mcpServers['myproj']).toEqual({
      command: 'mcpctl',
      args: ['mcp', '-p', 'myproj'],
    });
  });

  it('always merges with existing .mcp.json', async () => {
    const outPath = join(tmpDir, '.mcp.json');
    writeFileSync(outPath, JSON.stringify({
      mcpServers: { 'existing--server': { command: 'echo', args: [] } },
    }));

    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['claude', '--project', 'proj-1', '-o', outPath], { from: 'user' });

    const written = JSON.parse(readFileSync(outPath, 'utf-8'));
    expect(written.mcpServers['existing--server']).toBeDefined();
    expect(written.mcpServers['proj-1']).toEqual({
      command: 'mcpctl',
      args: ['mcp', '-p', 'proj-1'],
    });
    expect(output.join('\n')).toContain('2 server(s)');
  });

  it('adds inspect MCP server with --inspect', async () => {
    const outPath = join(tmpDir, '.mcp.json');
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['claude', '--inspect', '-o', outPath], { from: 'user' });

    const written = JSON.parse(readFileSync(outPath, 'utf-8'));
    expect(written.mcpServers['mcpctl-inspect']).toEqual({
      command: 'mcpctl',
      args: ['console', '--stdin-mcp'],
    });
    expect(output.join('\n')).toContain('1 server(s)');
  });

  it('adds both project and inspect with --project --inspect', async () => {
    const outPath = join(tmpDir, '.mcp.json');
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['claude', '--project', 'ha', '--inspect', '-o', outPath], { from: 'user' });

    const written = JSON.parse(readFileSync(outPath, 'utf-8'));
    expect(written.mcpServers['ha']).toBeDefined();
    expect(written.mcpServers['mcpctl-inspect']).toBeDefined();
    expect(output.join('\n')).toContain('2 server(s)');
  });

  it('backward compat: claude-generate still works', async () => {
    const outPath = join(tmpDir, '.mcp.json');
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['claude-generate', '--project', 'proj-1', '-o', outPath], { from: 'user' });

    const written = JSON.parse(readFileSync(outPath, 'utf-8'));
    expect(written.mcpServers['proj-1']).toEqual({
      command: 'mcpctl',
      args: ['mcp', '-p', 'proj-1'],
    });
  });

  it('uses project name as the server key', async () => {
    const outPath = join(tmpDir, '.mcp.json');
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['claude', '--project', 'my-fancy-project', '-o', outPath], { from: 'user' });

    const written = JSON.parse(readFileSync(outPath, 'utf-8'));
    expect(Object.keys(written.mcpServers)).toEqual(['my-fancy-project']);
  });
});

describe('config impersonate', () => {
  let client: ReturnType<typeof mockClient>;
  let output: string[];
  let tmpDir: string;
  const log = (...args: string[]) => output.push(args.join(' '));

  beforeEach(() => {
    client = mockClient();
    output = [];
    tmpDir = mkdtempSync(join(tmpdir(), 'mcpctl-config-impersonate-'));
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('impersonates a user and saves backup', async () => {
    saveCredentials({ token: 'admin-tok', mcpdUrl: 'http://localhost:3100', user: 'admin@test.com' }, { configDir: tmpDir });

    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['impersonate', 'other@test.com'], { from: 'user' });

    expect(client.post).toHaveBeenCalledWith('/api/v1/auth/impersonate', { email: 'other@test.com' });
    expect(output.join('\n')).toContain('Impersonating other@test.com');

    const creds = loadCredentials({ configDir: tmpDir });
    expect(creds!.user).toBe('other@test.com');
    expect(creds!.token).toBe('impersonated-tok');

    // Backup exists
    const backup = JSON.parse(readFileSync(join(tmpDir, 'credentials-backup'), 'utf-8'));
    expect(backup.user).toBe('admin@test.com');
  });

  it('quits impersonation and restores backup', async () => {
    // Set up current (impersonated) credentials
    saveCredentials({ token: 'impersonated-tok', mcpdUrl: 'http://localhost:3100', user: 'other@test.com' }, { configDir: tmpDir });
    // Set up backup (original) credentials
    writeFileSync(join(tmpDir, 'credentials-backup'), JSON.stringify({
      token: 'admin-tok', mcpdUrl: 'http://localhost:3100', user: 'admin@test.com',
    }));

    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['impersonate', '--quit'], { from: 'user' });

    expect(output.join('\n')).toContain('Returned to admin@test.com');

    const creds = loadCredentials({ configDir: tmpDir });
    expect(creds!.user).toBe('admin@test.com');
    expect(creds!.token).toBe('admin-tok');
  });

  it('errors when not logged in', async () => {
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['impersonate', 'other@test.com'], { from: 'user' });

    expect(output.join('\n')).toContain('Not logged in');
  });

  it('errors when quitting with no backup', async () => {
    const cmd = createConfigCommand(
      { configDeps: { configDir: tmpDir }, log },
      { client, credentialsDeps: { configDir: tmpDir }, log },
    );
    await cmd.parseAsync(['impersonate', '--quit'], { from: 'user' });

    expect(output.join('\n')).toContain('No impersonation session to quit');
  });
});
