import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Command } from 'commander';

import {
  registerUninstallCommand,
  reverseRuntimeAssets,
  reverseNpmrc,
  removeFramework,
  removeCli,
} from './uninstall.js';
import { writeManifest, createManifest } from '../runtime/install-manifest.js';

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

let tmpDir: string;
let mosaicHome: string;

beforeEach(() => {
  tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-uninstall-test-'));
  mosaicHome = join(tmpDir, 'mosaic');
  mkdirSync(mosaicHome, { recursive: true });
  vi.clearAllMocks();
});

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

// ─── command registration ────────────────────────────────────────────────────

describe('registerUninstallCommand', () => {
  it('registers an "uninstall" command on the program', () => {
    const program = new Command();
    program.exitOverride();
    registerUninstallCommand(program);
    const names = program.commands.map((c) => c.name());
    expect(names).toContain('uninstall');
  });

  it('registers the expected options', () => {
    const program = new Command();
    program.exitOverride();
    registerUninstallCommand(program);
    const cmd = program.commands.find((c) => c.name() === 'uninstall')!;
    const optNames = cmd.options.map((o) => o.long);
    expect(optNames).toContain('--framework');
    expect(optNames).toContain('--cli');
    expect(optNames).toContain('--gateway');
    expect(optNames).toContain('--all');
    expect(optNames).toContain('--keep-data');
    expect(optNames).toContain('--dry-run');
  });
});

// ─── reverseNpmrc ─────────────────────────────────────────────────────────────

describe('reverseNpmrc', () => {
  it('does nothing when .npmrc does not exist (heuristic mode, no manifest)', () => {
    // Should not throw; mosaicHome has no manifest and home has no .npmrc
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
    expect(() => reverseNpmrc(mosaicHome, true)).not.toThrow();
    warnSpy.mockRestore();
  });

  it('dry-run mode logs removal without mutating', () => {
    // Write a manifest with a known npmrc line
    writeManifest(
      mosaicHome,
      createManifest('0.0.24', 2, {
        npmrcLines: [
          '@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/',
        ],
      }),
    );
    // reverseNpmrc reads ~/.npmrc from actual homedir; dry-run won't touch anything
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    expect(() => reverseNpmrc(mosaicHome, true)).not.toThrow();
    logSpy.mockRestore();
  });
});

// ─── removeFramework ──────────────────────────────────────────────────────────

describe('removeFramework', () => {
  it('removes the entire directory when --keep-data is false', () => {
    writeFileSync(join(mosaicHome, 'AGENTS.md'), '# agents', 'utf8');

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    removeFramework(mosaicHome, false, false);
    logSpy.mockRestore();

    expect(existsSync(mosaicHome)).toBe(false);
  });

  it('preserves SOUL.md and memory/ when --keep-data is true', () => {
    writeFileSync(join(mosaicHome, 'AGENTS.md'), '# agents', 'utf8');
    writeFileSync(join(mosaicHome, 'SOUL.md'), '# soul', 'utf8');
    mkdirSync(join(mosaicHome, 'memory'), { recursive: true });
    writeFileSync(join(mosaicHome, 'memory', 'note.md'), 'note', 'utf8');

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    removeFramework(mosaicHome, true, false);
    logSpy.mockRestore();

    expect(existsSync(join(mosaicHome, 'SOUL.md'))).toBe(true);
    expect(existsSync(join(mosaicHome, 'memory'))).toBe(true);
    expect(existsSync(join(mosaicHome, 'AGENTS.md'))).toBe(false);
  });

  it('preserves USER.md and TOOLS.md when --keep-data is true', () => {
    writeFileSync(join(mosaicHome, 'AGENTS.md'), '# agents', 'utf8');
    writeFileSync(join(mosaicHome, 'USER.md'), '# user', 'utf8');
    writeFileSync(join(mosaicHome, 'TOOLS.md'), '# tools', 'utf8');

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    removeFramework(mosaicHome, true, false);
    logSpy.mockRestore();

    expect(existsSync(join(mosaicHome, 'USER.md'))).toBe(true);
    expect(existsSync(join(mosaicHome, 'TOOLS.md'))).toBe(true);
  });

  it('dry-run logs but does not remove', () => {
    writeFileSync(join(mosaicHome, 'AGENTS.md'), '# agents', 'utf8');
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    removeFramework(mosaicHome, false, true);
    logSpy.mockRestore();

    expect(existsSync(mosaicHome)).toBe(true);
  });

  it('handles missing mosaicHome gracefully', () => {
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    expect(() => removeFramework('/nonexistent/path', false, false)).not.toThrow();
    logSpy.mockRestore();
  });
});

// ─── reverseRuntimeAssets ─────────────────────────────────────────────────────

describe('reverseRuntimeAssets', () => {
  it('dry-run does not throw in heuristic mode (no manifest)', () => {
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

    expect(() => reverseRuntimeAssets(mosaicHome, true)).not.toThrow();

    logSpy.mockRestore();
    warnSpy.mockRestore();
  });

  it('restores backup when present (with manifest)', () => {
    // Create a fake dest and backup inside tmpDir
    const claudeDir = join(tmpDir, 'dot-claude');
    mkdirSync(claudeDir, { recursive: true });
    const dest = join(claudeDir, 'settings.json');
    const backup = join(claudeDir, 'settings.json.mosaic-bak-20260405120000');
    writeFileSync(dest, '{"current": true}', 'utf8');
    writeFileSync(backup, '{"original": true}', 'utf8');

    // Write a manifest pointing to these exact paths
    writeManifest(
      mosaicHome,
      createManifest('0.0.24', 2, {
        runtimeAssetCopies: [{ source: '/src/settings.json', dest, backup }],
      }),
    );

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    reverseRuntimeAssets(mosaicHome, false);
    logSpy.mockRestore();

    // Backup removed, dest has original content
    expect(existsSync(backup)).toBe(false);
    expect(readFileSync(dest, 'utf8')).toBe('{"original": true}');
  });

  it('removes managed copy when no backup present (with manifest)', () => {
    const claudeDir = join(tmpDir, 'dot-claude2');
    mkdirSync(claudeDir, { recursive: true });
    const dest = join(claudeDir, 'CLAUDE.md');
    writeFileSync(dest, '# managed', 'utf8');

    writeManifest(
      mosaicHome,
      createManifest('0.0.24', 2, {
        runtimeAssetCopies: [{ source: '/src/CLAUDE.md', dest }],
      }),
    );

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    reverseRuntimeAssets(mosaicHome, false);
    logSpy.mockRestore();

    expect(existsSync(dest)).toBe(false);
  });

  it('dry-run with manifest logs but does not remove', () => {
    const claudeDir = join(tmpDir, 'dot-claude3');
    mkdirSync(claudeDir, { recursive: true });
    const dest = join(claudeDir, 'hooks-config.json');
    writeFileSync(dest, '{}', 'utf8');

    writeManifest(
      mosaicHome,
      createManifest('0.0.24', 2, {
        runtimeAssetCopies: [{ source: '/src/hooks-config.json', dest }],
      }),
    );

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    reverseRuntimeAssets(mosaicHome, true);
    logSpy.mockRestore();

    // File should still exist in dry-run mode
    expect(existsSync(dest)).toBe(true);
  });
});

// ─── removeCli ────────────────────────────────────────────────────────────────

describe('removeCli', () => {
  it('dry-run logs the npm command without running it', () => {
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    removeCli(true);
    const output = logSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('npm uninstall -g @mosaicstack/mosaic');
    logSpy.mockRestore();
  });
});
