import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, rm, readFile, writeFile, mkdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { applyManagedHooks, removeManagedHooks, SOURCE_KEY } from '../../src/utils/hooks-materialiser.js';
import { MARKER_KEY } from '../../src/utils/sessionhook.js';

describe('hooks-materialiser', () => {
  let tmp: string;
  let settings: string;

  beforeEach(async () => {
    tmp = await mkdtemp(join(tmpdir(), 'mcpctl-hooks-'));
    settings = join(tmp, 'settings.json');
  });

  afterEach(async () => {
    await rm(tmp, { recursive: true, force: true });
  });

  it('writes a tagged hook from scratch when settings.json is missing', async () => {
    const result = await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo before' }],
    }, settings);

    expect(result.updated).toBe(true);
    const file = JSON.parse(await readFile(settings, 'utf-8'));
    expect(file.hooks.PreToolUse).toHaveLength(1);
    const entry = file.hooks.PreToolUse[0].hooks[0];
    expect(entry.command).toBe('echo before');
    expect(entry[MARKER_KEY]).toBe(true);
    expect(entry[SOURCE_KEY]).toBe('skill-a');
  });

  it('coexists with hooks owned by other skills', async () => {
    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo a' }],
    }, settings);
    await applyManagedHooks('skill-b', {
      PreToolUse: [{ type: 'command', command: 'echo b' }],
    }, settings);

    const file = JSON.parse(await readFile(settings, 'utf-8'));
    const all = file.hooks.PreToolUse.flatMap((g: { hooks: Array<{ command: string; [k: string]: unknown }> }) => g.hooks);
    expect(all.find((e: { command: string }) => e.command === 'echo a')).toBeDefined();
    expect(all.find((e: { command: string }) => e.command === 'echo b')).toBeDefined();
    expect(all).toHaveLength(2);
  });

  it('preserves user-added hooks (no marker)', async () => {
    await mkdir(tmp, { recursive: true });
    await writeFile(settings, JSON.stringify({
      hooks: {
        PreToolUse: [{ hooks: [{ type: 'command', command: 'echo user' }] }],
      },
    }));

    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo a' }],
    }, settings);

    const file = JSON.parse(await readFile(settings, 'utf-8'));
    const all = file.hooks.PreToolUse.flatMap((g: { hooks: Array<{ command: string; [k: string]: unknown }> }) => g.hooks);
    expect(all.find((e: { command: string }) => e.command === 'echo user')).toBeDefined();
    expect(all.find((e: { command: string; [k: string]: unknown }) => e.command === 'echo a' && e[MARKER_KEY] === true)).toBeDefined();
  });

  it('updating a skill replaces its old entries (does not duplicate)', async () => {
    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo old' }],
    }, settings);
    const second = await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo new' }],
    }, settings);

    expect(second.updated).toBe(true);
    const file = JSON.parse(await readFile(settings, 'utf-8'));
    const all = file.hooks.PreToolUse.flatMap((g: { hooks: Array<{ command: string; [k: string]: unknown }> }) => g.hooks);
    const ours = all.filter((e: { [k: string]: unknown }) => e[SOURCE_KEY] === 'skill-a');
    expect(ours).toHaveLength(1);
    expect((ours[0] as { command: string }).command).toBe('echo new');
  });

  it('shrinking a skill drops events it no longer declares', async () => {
    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo pre' }],
      PostToolUse: [{ type: 'command', command: 'echo post' }],
    }, settings);

    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo pre' }],
      // PostToolUse omitted → should be dropped
    }, settings);

    const file = JSON.parse(await readFile(settings, 'utf-8'));
    expect(file.hooks.PreToolUse).toBeDefined();
    expect(file.hooks.PostToolUse).toBeUndefined();
  });

  it('removeManagedHooks drops only the named source', async () => {
    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo a' }],
    }, settings);
    await applyManagedHooks('skill-b', {
      PreToolUse: [{ type: 'command', command: 'echo b' }],
    }, settings);

    const removed = await removeManagedHooks('skill-a', settings);
    expect(removed.removed).toBe(true);

    const file = JSON.parse(await readFile(settings, 'utf-8'));
    const all = file.hooks.PreToolUse.flatMap((g: { hooks: Array<{ command: string; [k: string]: unknown }> }) => g.hooks);
    expect(all).toHaveLength(1);
    expect((all[0] as { command: string }).command).toBe('echo b');
  });

  it('removeManagedHooks is a no-op when the source has no entries', async () => {
    const result = await removeManagedHooks('never-installed', settings);
    expect(result.removed).toBe(false);
  });

  it('handles multiple hook events independently', async () => {
    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo pre' }],
      PostToolUse: [{ type: 'command', command: 'echo post' }],
      SessionStart: [{ type: 'command', command: 'echo start' }],
    }, settings);

    const file = JSON.parse(await readFile(settings, 'utf-8'));
    expect(file.hooks.PreToolUse).toBeDefined();
    expect(file.hooks.PostToolUse).toBeDefined();
    expect(file.hooks.SessionStart).toBeDefined();
  });

  it('idempotent — re-applying the same hooks reports updated=true on first call only', async () => {
    const first = await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo a' }],
    }, settings);
    expect(first.updated).toBe(true);

    // Re-applying ALWAYS rewrites our entry (we don't deep-equal them
    // for "no change"), but the resulting file is byte-identical except
    // for ordering. The test just confirms the file remains valid + well-shaped.
    const second = await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo a' }],
    }, settings);
    // updated=true is acceptable here; we replaced+re-added our entry.
    expect(second.updated).toBe(true);

    const file = JSON.parse(await readFile(settings, 'utf-8'));
    const all = file.hooks.PreToolUse.flatMap((g: { hooks: Array<{ command: string; [k: string]: unknown }> }) => g.hooks);
    const ours = all.filter((e: { [k: string]: unknown }) => e[SOURCE_KEY] === 'skill-a');
    expect(ours).toHaveLength(1);
  });

  it('survives empty settings.json', async () => {
    await writeFile(settings, '');
    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo a' }],
    }, settings);
    const file = JSON.parse(await readFile(settings, 'utf-8'));
    expect(file.hooks.PreToolUse).toHaveLength(1);
  });

  it('survives JSONC line comments in settings.json', async () => {
    await writeFile(settings, '// preamble\n{ "hooks": {} }\n');
    await applyManagedHooks('skill-a', {
      PreToolUse: [{ type: 'command', command: 'echo a' }],
    }, settings);
    const file = JSON.parse(await readFile(settings, 'utf-8'));
    expect(file.hooks.PreToolUse).toHaveLength(1);
  });
});
