import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Command } from 'commander';
import { registerConfigCommand } from './config.js';

// ── helpers ──────────────────────────────────────────────────────────────────

/** Build a fresh Command tree with the config command registered. */
function buildProgram(): Command {
  const program = new Command();
  program.exitOverride(); // prevent process.exit during tests
  registerConfigCommand(program);
  return program;
}

/** Locate the 'config' command registered on the root program. */
function getConfigCmd(program: Command): Command {
  const found = program.commands.find((c) => c.name() === 'config');
  if (!found) throw new Error('config command not found');
  return found;
}

// ── subcommand registration ───────────────────────────────────────────────────

describe('registerConfigCommand', () => {
  it('registers a "config" command on the program', () => {
    const program = buildProgram();
    const names = program.commands.map((c) => c.name());
    expect(names).toContain('config');
  });

  it('registers exactly the required subcommands', () => {
    const program = buildProgram();
    const config = getConfigCmd(program);
    const subs = config.commands.map((c) => c.name()).sort();
    expect(subs).toEqual(['edit', 'get', 'hooks', 'path', 'set', 'show']);
  });

  it('registers hooks sub-subcommands: list, enable, disable', () => {
    const program = buildProgram();
    const config = getConfigCmd(program);
    const hooks = config.commands.find((c) => c.name() === 'hooks');
    expect(hooks).toBeDefined();
    const hookSubs = hooks!.commands.map((c) => c.name()).sort();
    expect(hookSubs).toEqual(['disable', 'enable', 'list']);
  });
});

// ── mock config service ───────────────────────────────────────────────────────

const mockSoul = {
  agentName: 'TestBot',
  roleDescription: 'test role',
  communicationStyle: 'direct' as const,
};
const mockUser = { userName: 'Tester', pronouns: 'they/them', timezone: 'UTC' };
const mockTools = { credentialsLocation: '/dev/null' };

const mockSvc = {
  readSoul: vi.fn().mockResolvedValue(mockSoul),
  readUser: vi.fn().mockResolvedValue(mockUser),
  readTools: vi.fn().mockResolvedValue(mockTools),
  writeSoul: vi.fn().mockResolvedValue(undefined),
  writeUser: vi.fn().mockResolvedValue(undefined),
  writeTools: vi.fn().mockResolvedValue(undefined),
  syncFramework: vi.fn().mockResolvedValue(undefined),
  readAll: vi.fn().mockResolvedValue({ soul: mockSoul, user: mockUser, tools: mockTools }),
  getValue: vi.fn().mockResolvedValue('TestBot'),
  setValue: vi.fn().mockResolvedValue('OldBot'),
  getConfigPath: vi
    .fn()
    .mockImplementation((section?: string) =>
      section
        ? `/home/user/.config/mosaic/${section.toUpperCase()}.md`
        : '/home/user/.config/mosaic',
    ),
  isInitialized: vi.fn().mockReturnValue(true),
};

// Mock the config-service module so commands use our mock.
vi.mock('../config/config-service.js', () => ({
  createConfigService: vi.fn(() => mockSvc),
}));

// Also mock child_process for the edit command.
vi.mock('node:child_process', () => ({
  spawnSync: vi.fn().mockReturnValue({ status: 0, error: undefined }),
}));

// ── config show ───────────────────────────────────────────────────────────────

describe('config show', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.isInitialized.mockReturnValue(true);
    mockSvc.readAll.mockResolvedValue({ soul: mockSoul, user: mockUser, tools: mockTools });
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('calls readAll() and prints a table by default', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'show']);
    expect(mockSvc.readAll).toHaveBeenCalledOnce();
    // Should have printed something
    expect(consoleSpy).toHaveBeenCalled();
  });

  it('prints JSON when --format json is passed', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'show', '--format', 'json']);
    expect(mockSvc.readAll).toHaveBeenCalledOnce();
    // Verify JSON was logged
    const allOutput = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(allOutput).toContain('"agentName"');
  });
});

// ── config get ────────────────────────────────────────────────────────────────

describe('config get', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.isInitialized.mockReturnValue(true);
    mockSvc.getValue.mockResolvedValue('TestBot');
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('delegates to getValue() with the provided key', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'get', 'soul.agentName']);
    expect(mockSvc.getValue).toHaveBeenCalledWith('soul.agentName');
  });

  it('prints the returned value', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'get', 'soul.agentName']);
    expect(consoleSpy).toHaveBeenCalledWith('TestBot');
  });
});

// ── config set ────────────────────────────────────────────────────────────────

describe('config set', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.isInitialized.mockReturnValue(true);
    mockSvc.setValue.mockResolvedValue('OldBot');
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('delegates to setValue() with key and value', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'set', 'soul.agentName', 'NewBot']);
    expect(mockSvc.setValue).toHaveBeenCalledWith('soul.agentName', 'NewBot');
  });

  it('prints old and new values', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'set', 'soul.agentName', 'NewBot']);
    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('OldBot');
    expect(output).toContain('NewBot');
  });
});

// ── config path ───────────────────────────────────────────────────────────────

describe('config path', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.getConfigPath.mockImplementation((section?: string) =>
      section
        ? `/home/user/.config/mosaic/${section.toUpperCase()}.md`
        : '/home/user/.config/mosaic',
    );
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('prints the mosaicHome directory when no section is specified', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'path']);
    expect(mockSvc.getConfigPath).toHaveBeenCalledWith();
    expect(consoleSpy).toHaveBeenCalledWith('/home/user/.config/mosaic');
  });

  it('prints the section file path when --section is given', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'path', '--section', 'soul']);
    expect(mockSvc.getConfigPath).toHaveBeenCalledWith('soul');
    expect(consoleSpy).toHaveBeenCalledWith('/home/user/.config/mosaic/SOUL.md');
  });
});

// ── config edit ───────────────────────────────────────────────────────────────

describe('config edit', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;
  let spawnSyncMock: ReturnType<typeof vi.fn>;

  beforeEach(async () => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.isInitialized.mockReturnValue(true);
    mockSvc.readAll.mockResolvedValue({ soul: mockSoul, user: mockUser, tools: mockTools });
    mockSvc.getConfigPath.mockImplementation((section?: string) =>
      section
        ? `/home/user/.config/mosaic/${section.toUpperCase()}.md`
        : '/home/user/.config/mosaic',
    );

    // Re-import to get the mock reference
    const cp = await import('node:child_process');
    spawnSyncMock = cp.spawnSync as ReturnType<typeof vi.fn>;
    spawnSyncMock.mockReturnValue({ status: 0, error: undefined });
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('calls spawnSync with the editor binary and config path', async () => {
    process.env['EDITOR'] = 'nano';
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'edit']);
    expect(spawnSyncMock).toHaveBeenCalledWith(
      'nano',
      ['/home/user/.config/mosaic'],
      expect.objectContaining({ stdio: 'inherit' }),
    );
    delete process.env['EDITOR'];
  });

  it('falls back to "vi" when EDITOR is not set', async () => {
    delete process.env['EDITOR'];
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'edit']);
    expect(spawnSyncMock).toHaveBeenCalledWith('vi', expect.any(Array), expect.any(Object));
  });

  it('opens the section-specific file when --section is provided', async () => {
    process.env['EDITOR'] = 'code';
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'edit', '--section', 'soul']);
    expect(spawnSyncMock).toHaveBeenCalledWith(
      'code',
      ['/home/user/.config/mosaic/SOUL.md'],
      expect.any(Object),
    );
    delete process.env['EDITOR'];
  });
});

// ── config hooks ─────────────────────────────────────────────────────────────

const MOCK_HOOKS_CONFIG = JSON.stringify({
  name: 'Test Hooks',
  hooks: {
    PostToolUse: [
      {
        matcher: 'Write|Edit',
        hooks: [{ type: 'command', command: 'bash', args: ['-c', 'echo'] }],
      },
    ],
  },
});

const MOCK_HOOKS_WITH_DISABLED = JSON.stringify({
  name: 'Test Hooks',
  hooks: {
    PostToolUse: [{ matcher: 'Write|Edit', hooks: [] }],
    _disabled_PreToolUse: [{ matcher: 'Bash', hooks: [] }],
  },
});

vi.mock('node:fs', () => ({
  existsSync: vi.fn(),
  readFileSync: vi.fn(),
  writeFileSync: vi.fn(),
}));

async function getFsMock() {
  const fs = await import('node:fs');
  return {
    existsSync: fs.existsSync as ReturnType<typeof vi.fn>,
    readFileSync: fs.readFileSync as ReturnType<typeof vi.fn>,
    writeFileSync: fs.writeFileSync as ReturnType<typeof vi.fn>,
  };
}

describe('config hooks list', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(async () => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.isInitialized.mockReturnValue(true);
    const fs = await getFsMock();
    fs.existsSync.mockReturnValue(true);
    fs.readFileSync.mockReturnValue(MOCK_HOOKS_CONFIG);
    // Ensure CLAUDE_HOME is set to a stable value for tests
    process.env['CLAUDE_HOME'] = '/tmp/claude-test';
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    delete process.env['CLAUDE_HOME'];
  });

  it('lists hooks with enabled/disabled status', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'hooks', 'list']);
    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('PostToolUse');
    expect(output).toContain('enabled');
  });

  it('shows disabled hooks from MOCK_HOOKS_WITH_DISABLED', async () => {
    const fs = await getFsMock();
    fs.readFileSync.mockReturnValue(MOCK_HOOKS_WITH_DISABLED);

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'hooks', 'list']);
    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('disabled');
    expect(output).toContain('PreToolUse');
  });

  it('prints a message when hooks-config.json is missing', async () => {
    const fs = await getFsMock();
    fs.existsSync.mockReturnValue(false);

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'hooks', 'list']);
    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('No hooks-config.json');
  });
});

describe('config hooks disable / enable', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(async () => {
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.isInitialized.mockReturnValue(true);
    const fs = await getFsMock();
    fs.existsSync.mockReturnValue(true);
    fs.readFileSync.mockReturnValue(MOCK_HOOKS_CONFIG);
    process.env['CLAUDE_HOME'] = '/tmp/claude-test';
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    delete process.env['CLAUDE_HOME'];
  });

  it('disables a hook by event name and writes updated config', async () => {
    const fs = await getFsMock();

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'hooks', 'disable', 'PostToolUse']);

    expect(fs.writeFileSync).toHaveBeenCalled();
    const written = JSON.parse((fs.writeFileSync.mock.calls[0] as [string, string])[1]) as {
      hooks: Record<string, unknown>;
    };
    expect(written.hooks['_disabled_PostToolUse']).toBeDefined();
    expect(written.hooks['PostToolUse']).toBeUndefined();
    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('disabled'));
  });

  it('enables a disabled hook and writes updated config', async () => {
    const fs = await getFsMock();
    fs.readFileSync.mockReturnValue(MOCK_HOOKS_WITH_DISABLED);

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'config', 'hooks', 'enable', 'PreToolUse']);

    expect(fs.writeFileSync).toHaveBeenCalled();
    const written = JSON.parse((fs.writeFileSync.mock.calls[0] as [string, string])[1]) as {
      hooks: Record<string, unknown>;
    };
    expect(written.hooks['PreToolUse']).toBeDefined();
    expect(written.hooks['_disabled_PreToolUse']).toBeUndefined();
    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('enabled'));
  });
});

// ── not-initialized guard ────────────────────────────────────────────────────

describe('not-initialized guard', () => {
  let consoleErrorSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    vi.clearAllMocks();
    mockSvc.isInitialized.mockReturnValue(false);
  });

  afterEach(() => {
    consoleErrorSpy.mockRestore();
    mockSvc.isInitialized.mockReturnValue(true);
  });

  it('prints a helpful message when config is missing (show)', async () => {
    const program = buildProgram();
    // process.exit is intercepted; catch the resulting error from exitOverride
    await expect(program.parseAsync(['node', 'mosaic', 'config', 'show'])).rejects.toThrow();
    expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('mosaic wizard'));
  });
});
