import { describe, it, expect, vi, beforeEach } from 'vitest';
import { hooksPreviewStage } from './hooks-preview.js';
import type { WizardState } from '../types.js';

// ── Mock fs ───────────────────────────────────────────────────────────────────

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockExistsSync = vi.fn<any>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockReadFileSync = vi.fn<any>();

vi.mock('node:fs', () => ({
  existsSync: (p: string) => mockExistsSync(p),
  readFileSync: (p: string, enc: string) => mockReadFileSync(p, enc),
}));

// ── Mock prompter ─────────────────────────────────────────────────────────────

function buildPrompter(confirmAnswer = true) {
  return {
    intro: vi.fn(),
    outro: vi.fn(),
    note: vi.fn(),
    log: vi.fn(),
    warn: vi.fn(),
    text: vi.fn(),
    confirm: vi.fn().mockResolvedValue(confirmAnswer),
    select: vi.fn(),
    multiselect: vi.fn(),
    groupMultiselect: vi.fn(),
    spinner: vi.fn().mockReturnValue({ update: vi.fn(), stop: vi.fn() }),
    separator: vi.fn(),
  };
}

// ── Fixture ───────────────────────────────────────────────────────────────────

const MINIMAL_HOOKS_CONFIG = JSON.stringify({
  name: 'Test Hooks',
  description: 'For testing',
  version: '1.0.0',
  hooks: {
    PostToolUse: [
      {
        matcher: 'Write|Edit',
        hooks: [
          {
            type: 'command',
            command: 'bash',
            args: ['-c', 'echo hello'],
          },
        ],
      },
    ],
  },
});

function makeState(overrides: Partial<WizardState> = {}): WizardState {
  return {
    mosaicHome: '/home/user/.config/mosaic',
    sourceDir: '/opt/mosaic',
    mode: 'quick',
    installAction: 'fresh',
    soul: {},
    user: {},
    tools: {},
    runtimes: { detected: ['claude'], mcpConfigured: true },
    selectedSkills: [],
    ...overrides,
  };
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('hooksPreviewStage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('skips entirely when claude is not in detected runtimes', async () => {
    const p = buildPrompter();
    const state = makeState({ runtimes: { detected: ['codex'], mcpConfigured: false } });

    await hooksPreviewStage(p, state);

    expect(p.separator).not.toHaveBeenCalled();
    expect(p.confirm).not.toHaveBeenCalled();
    expect(state.hooks).toBeUndefined();
  });

  it('warns and returns when hooks-config.json is not found', async () => {
    mockExistsSync.mockReturnValue(false);
    const p = buildPrompter();
    const state = makeState();

    await hooksPreviewStage(p, state);

    expect(p.warn).toHaveBeenCalledWith(expect.stringContaining('hooks-config.json'));
    expect(p.confirm).not.toHaveBeenCalled();
    expect(state.hooks).toBeUndefined();
  });

  it('displays hook details and sets accepted=true when user confirms', async () => {
    mockExistsSync.mockReturnValueOnce(true);
    mockReadFileSync.mockReturnValueOnce(MINIMAL_HOOKS_CONFIG);

    const p = buildPrompter(true);
    const state = makeState();

    await hooksPreviewStage(p, state);

    expect(p.note).toHaveBeenCalled();
    expect(p.confirm).toHaveBeenCalledWith(
      expect.objectContaining({ message: expect.stringContaining('Install') }),
    );
    expect(state.hooks?.accepted).toBe(true);
    expect(state.hooks?.acceptedAt).toBeTruthy();
  });

  it('sets accepted=false when user declines', async () => {
    mockExistsSync.mockReturnValueOnce(true);
    mockReadFileSync.mockReturnValueOnce(MINIMAL_HOOKS_CONFIG);

    const p = buildPrompter(false);
    const state = makeState();

    await hooksPreviewStage(p, state);

    expect(state.hooks?.accepted).toBe(false);
    expect(state.hooks?.acceptedAt).toBeUndefined();
    // Should print a skip note
    expect(p.note).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('skipped'));
  });

  it('tries mosaicHome fallback path when sourceDir file is absent', async () => {
    // First existsSync call (sourceDir path) → false; second (mosaicHome) → true
    mockExistsSync.mockReturnValueOnce(false).mockReturnValueOnce(true);
    mockReadFileSync.mockReturnValueOnce(MINIMAL_HOOKS_CONFIG);

    const p = buildPrompter(true);
    const state = makeState();

    await hooksPreviewStage(p, state);

    expect(state.hooks?.accepted).toBe(true);
  });

  it('warns when the config file is malformed JSON', async () => {
    mockExistsSync.mockReturnValueOnce(true);
    mockReadFileSync.mockReturnValueOnce('NOT_JSON{{{');

    const p = buildPrompter();
    const state = makeState();

    await hooksPreviewStage(p, state);

    expect(p.warn).toHaveBeenCalled();
    expect(state.hooks).toBeUndefined();
  });
});
