/**
 * Cassette System Unit Tests
 *
 * Verifies normalizer, key generation, and store in isolation.
 * These run without any real processes or API calls.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { normalizePrompt, stripPromptFromArgs } from './normalizer.js';
import { hashWorktreeFiles, buildCassetteKey } from './key.js';
import { CassetteStore } from './store.js';
import type { CassetteEntry, CassetteKey } from './types.js';

// ---------------------------------------------------------------------------
// Normalizer
// ---------------------------------------------------------------------------

describe('normalizePrompt', () => {
  it('strips UUIDs', () => {
    const prompt = 'Agent 550e8400-e29b-41d4-a716-446655440000 is running task abc123ef-0000-0000-0000-000000000000';
    const result = normalizePrompt(prompt, '');
    expect(result).not.toContain('550e8400');
    expect(result).not.toContain('abc123ef');
    expect(result).toContain('__UUID__');
  });

  it('strips workspace root path', () => {
    const workspaceRoot = '/tmp/cw-test-abc123';
    const prompt = `Working directory: ${workspaceRoot}/agent-workdirs/my-agent`;
    const result = normalizePrompt(prompt, workspaceRoot);
    expect(result).not.toContain(workspaceRoot);
    expect(result).toContain('__WORKSPACE__');
  });

  it('strips ISO timestamps', () => {
    const prompt = 'Started at 2026-03-01T14:30:00Z, last seen 2026-03-01T14:35:22.456Z';
    const result = normalizePrompt(prompt, '');
    expect(result).not.toContain('2026-03-01');
    expect(result).toContain('__TIMESTAMP__');
  });

  it('strips session numbers', () => {
    const prompt = 'Resuming session 3 with agent session-42';
    const result = normalizePrompt(prompt, '');
    expect(result).toContain('session__N__');
    expect(result).not.toContain('session 3');
    expect(result).not.toContain('session-42');
  });

  it('leaves static content unchanged', () => {
    const prompt = 'You are a Worker agent. Execute the assigned coding task.';
    const result = normalizePrompt(prompt, '/tmp/ws');
    expect(result).toBe(prompt);
  });

  it('strips nanoid strings (21-char alphanumeric)', () => {
    const nanoid = 'V1StGXR8_Z5jdHi6B-myT';
    const prompt = `Agent worktree: /tmp/cw-preview-${nanoid}/app`;
    const result = normalizePrompt(prompt, '');
    expect(result).not.toContain(nanoid);
    expect(result).toContain('__ID__');
  });

  it('strips workspace root before UUID replacement to avoid double-normalizing', () => {
    const workspaceRoot = '/tmp/cw-test-abc123';
    const uuid = '550e8400-e29b-41d4-a716-446655440000';
    const prompt = `Dir: ${workspaceRoot}/agents/${uuid}`;
    const result = normalizePrompt(prompt, workspaceRoot);
    expect(result).toBe('Dir: __WORKSPACE__/agents/__UUID__');
  });
});

describe('stripPromptFromArgs', () => {
  it('strips -p <prompt> style (Claude native)', () => {
    const prompt = 'Do the task.';
    const args = ['--dangerously-skip-permissions', '--verbose', '-p', prompt, '--output-format', 'stream-json'];
    const result = stripPromptFromArgs(args, prompt);
    expect(result).toEqual(['--dangerously-skip-permissions', '--verbose', '--output-format', 'stream-json']);
  });

  it('strips --prompt <prompt> style', () => {
    const prompt = 'Do the task.';
    const args = ['--flag', '--prompt', prompt, '--json'];
    const result = stripPromptFromArgs(args, prompt);
    expect(result).toEqual(['--flag', '--json']);
  });

  it('strips bare positional prompt', () => {
    const prompt = 'Do the task.';
    const args = ['--full-auto', prompt];
    const result = stripPromptFromArgs(args, prompt);
    expect(result).toEqual(['--full-auto']);
  });

  it('returns unchanged args when prompt is empty', () => {
    const args = ['--flag', '--value'];
    expect(stripPromptFromArgs(args, '')).toEqual(args);
  });
});

// ---------------------------------------------------------------------------
// Key generation
// ---------------------------------------------------------------------------

describe('buildCassetteKey', () => {
  const baseKey: CassetteKey = {
    normalizedPrompt: 'You are a Worker agent.',
    providerName: 'claude',
    modelArgs: ['--dangerously-skip-permissions', '--verbose', '--output-format', 'stream-json'],
    worktreeHash: 'empty',
  };

  it('produces a 32-char hex string', () => {
    const key = buildCassetteKey(baseKey);
    expect(key).toMatch(/^[0-9a-f]{32}$/);
  });

  it('is deterministic for the same key', () => {
    expect(buildCassetteKey(baseKey)).toBe(buildCassetteKey(baseKey));
  });

  it('differs when normalizedPrompt changes', () => {
    const key2 = { ...baseKey, normalizedPrompt: 'You are a Discuss agent.' };
    expect(buildCassetteKey(baseKey)).not.toBe(buildCassetteKey(key2));
  });

  it('differs when providerName changes', () => {
    const key2 = { ...baseKey, providerName: 'codex' };
    expect(buildCassetteKey(baseKey)).not.toBe(buildCassetteKey(key2));
  });

  it('differs when worktreeHash changes', () => {
    const key2 = { ...baseKey, worktreeHash: 'abcdef1234567890' };
    expect(buildCassetteKey(baseKey)).not.toBe(buildCassetteKey(key2));
  });

  it('is stable regardless of modelArgs insertion order', () => {
    const key1 = { ...baseKey, modelArgs: ['--verbose', '--dangerously-skip-permissions'] };
    const key2 = { ...baseKey, modelArgs: ['--dangerously-skip-permissions', '--verbose'] };
    expect(buildCassetteKey(key1)).toBe(buildCassetteKey(key2));
  });
});

describe('hashWorktreeFiles', () => {
  it('returns "empty" for a non-existent directory', () => {
    expect(hashWorktreeFiles('/does/not/exist')).toBe('empty');
  });

  it('returns "empty" for a directory with only hidden files', () => {
    const dir = mkdtempSync(join(tmpdir(), 'cw-hash-test-'));
    try {
      // Only hidden entries present
      const { mkdirSync } = require('node:fs');
      mkdirSync(join(dir, '.git'));
      expect(hashWorktreeFiles(dir)).toBe('empty');
    } finally {
      rmSync(dir, { recursive: true });
    }
  });

  it('produces a 16-char hex string for a directory with files', () => {
    const dir = mkdtempSync(join(tmpdir(), 'cw-hash-test-'));
    try {
      const { writeFileSync } = require('node:fs');
      writeFileSync(join(dir, 'index.ts'), 'export const x = 1;');
      const hash = hashWorktreeFiles(dir);
      expect(hash).toMatch(/^[0-9a-f]{16}$/);
    } finally {
      rmSync(dir, { recursive: true });
    }
  });

  it('changes when file content changes', () => {
    const dir = mkdtempSync(join(tmpdir(), 'cw-hash-test-'));
    try {
      const { writeFileSync } = require('node:fs');
      writeFileSync(join(dir, 'index.ts'), 'export const x = 1;');
      const hash1 = hashWorktreeFiles(dir);
      writeFileSync(join(dir, 'index.ts'), 'export const x = 2;');
      const hash2 = hashWorktreeFiles(dir);
      expect(hash1).not.toBe(hash2);
    } finally {
      rmSync(dir, { recursive: true });
    }
  });
});

// ---------------------------------------------------------------------------
// CassetteStore
// ---------------------------------------------------------------------------

describe('CassetteStore', () => {
  let dir: string;
  let store: CassetteStore;

  const key: CassetteKey = {
    normalizedPrompt: 'Test prompt',
    providerName: 'claude',
    modelArgs: ['--verbose'],
    worktreeHash: 'empty',
  };

  const entry: CassetteEntry = {
    version: 1,
    key,
    recording: {
      jsonlLines: ['{"type":"system","session_id":"test-session"}', '{"type":"result","subtype":"success"}'],
      signalJson: { status: 'done', message: 'Task completed' },
      exitCode: 0,
      recordedAt: '2026-03-01T00:00:00.000Z',
    },
  };

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'cw-store-test-'));
    store = new CassetteStore(dir);
  });

  it('returns null for unknown key', () => {
    expect(store.find(key)).toBeNull();
  });

  it('round-trips a cassette entry', () => {
    store.save(key, entry);
    const loaded = store.find(key);
    expect(loaded).not.toBeNull();
    expect(loaded?.recording.signalJson).toEqual({ status: 'done', message: 'Task completed' });
    expect(loaded?.recording.jsonlLines).toHaveLength(2);
  });

  it('overwrites an existing cassette', () => {
    store.save(key, entry);
    const updated: CassetteEntry = {
      ...entry,
      recording: { ...entry.recording, jsonlLines: ['new line'], recordedAt: '2026-03-02T00:00:00.000Z' },
    };
    store.save(key, updated);
    const loaded = store.find(key);
    expect(loaded?.recording.jsonlLines).toEqual(['new line']);
  });

  it('uses same file for same key', () => {
    store.save(key, entry);
    const { readdirSync } = require('node:fs');
    const files = readdirSync(dir).filter((f: string) => f.endsWith('.json'));
    expect(files).toHaveLength(1);

    store.save(key, entry); // overwrite
    const files2 = readdirSync(dir).filter((f: string) => f.endsWith('.json'));
    expect(files2).toHaveLength(1);
  });

  it('uses different files for different keys', () => {
    const key2: CassetteKey = { ...key, providerName: 'codex' };
    store.save(key, entry);
    store.save(key2, { ...entry, key: key2 });

    const { readdirSync } = require('node:fs');
    const files = readdirSync(dir).filter((f: string) => f.endsWith('.json'));
    expect(files).toHaveLength(2);
  });
});
