import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseSpecHeader, listSpecs } from '../../src/specs/listSpecs.js';

describe('parseSpecHeader', () => {
  test('extracts original prompt + outcome + steps + expected', () => {
    const src = [
      `import { test, expect } from '@playwright/test';`,
      ``,
      `/**`,
      ` * Generated by Hover on 2026-05-29.`,
      ` * Original prompt: log in then add a todo`,
      ` * Outcome: Logged in, todo added.`,
      ` *`,
      ` * Steps:`,
      ` *   1. Open /`,
      ` *   2. Type alice into Email`,
      ` *   3. Click Submit button`,
      ` *`,
      ` * Expected:`,
      ` *   • Welcome heading shows alice`,
      ` *   • Todo list contains "buy milk"`,
      ` */`,
      `test('demo', async ({ page }) => {`,
      `  await page.goto('/');`,
      `});`,
    ].join('\n');
    const h = parseSpecHeader(src);
    expect(h.originalPrompt).toBe('log in then add a todo');
    expect(h.outcome).toBe('Logged in, todo added.');
    expect(h.steps).toEqual([
      'Open /',
      'Type alice into Email',
      'Click Submit button',
    ]);
    expect(h.expected).toEqual([
      'Welcome heading shows alice',
      'Todo list contains "buy milk"',
    ]);
  });

  test('returns all-null when there is no JSDoc block', () => {
    const src = `import { test } from '@playwright/test';\ntest('x', async () => {});\n`;
    const h = parseSpecHeader(src);
    expect(h.originalPrompt).toBeNull();
    expect(h.outcome).toBeNull();
    expect(h.steps).toEqual([]);
    expect(h.expected).toEqual([]);
  });

  test('ignores trailing JSDoc that appears AFTER the first test() — only header block counts', () => {
    const src = [
      `import { test } from '@playwright/test';`,
      `test('x', async () => {});`,
      `/**`,
      ` * Original prompt: this should not be picked up`,
      ` */`,
    ].join('\n');
    const h = parseSpecHeader(src);
    expect(h.originalPrompt).toBeNull();
  });

  test('handles header with only Original prompt + nothing else', () => {
    const src = [
      `/**`,
      ` * Original prompt: just log in`,
      ` */`,
      `test('x', async () => {});`,
    ].join('\n');
    const h = parseSpecHeader(src);
    expect(h.originalPrompt).toBe('just log in');
    expect(h.outcome).toBeNull();
    expect(h.steps).toEqual([]);
    expect(h.expected).toEqual([]);
  });

  test('trims surrounding whitespace from prompt', () => {
    const src = [
      `/**`,
      ` * Original prompt:    log in    `,
      ` */`,
      `test('x', async () => {});`,
    ].join('\n');
    expect(parseSpecHeader(src).originalPrompt).toBe('log in');
  });

  test('stops list extraction at next section marker', () => {
    const src = [
      `/**`,
      ` * Steps:`,
      ` *   1. one`,
      ` *   2. two`,
      ` * Expected:`,
      ` *   • result`,
      ` */`,
      `test('x', async () => {});`,
    ].join('\n');
    const h = parseSpecHeader(src);
    expect(h.steps).toEqual(['one', 'two']);
    expect(h.expected).toEqual(['result']);
  });

  test('stops list at blank JSDoc line', () => {
    const src = [
      `/**`,
      ` * Steps:`,
      ` *   1. only step`,
      ` *`,
      ` * (some trailing notes)`,
      ` */`,
      `test('x', async () => {});`,
    ].join('\n');
    expect(parseSpecHeader(src).steps).toEqual(['only step']);
  });

  test('tolerates Hover spec produced by writeSpec — round-trip header survives', () => {
    // Real shape that writeSpec.ts emits — fixture lives in the repo.
    const src = [
      `import { test, expect } from '@playwright/test';`,
      ``,
      `/**`,
      ` * Generated by Hover on 2026-05-29.`,
      ` * Original prompt: log in then click + 1 three times and verify the counter, then add a todo and verify it`,
      ` * Outcome: Logged in, counter is 03, todo "verify hover" added.`,
      ` *`,
      ` * Steps:`,
      ` *   1. Open /`,
      ` *   2. Type "claude@sparkplay.io" into Email`,
      ` *   3. Type "demo1234" into Password`,
      ` *   4. Click Submit button`,
      ` *`,
      ` * Expected:`,
      ` *   • welcome heading shows the logged-in email`,
      ` *   • counter reads 03`,
      ` *`,
      ` * Selectors prefer getByRole / getByLabel / getByTestId — generated from`,
      ` * the agent's natural-language element descriptions, not raw CSS ids,`,
      ` * so the spec survives markup changes that don't touch semantics.`,
      ` */`,
      `test('demo', async ({ page }) => {});`,
    ].join('\n');
    const h = parseSpecHeader(src);
    expect(h.originalPrompt).toContain('log in then click + 1');
    expect(h.steps.length).toBe(4);
    expect(h.expected.length).toBe(2);
  });
});

describe('listSpecs', () => {
  let tmp: string;

  beforeEach(() => {
    tmp = mkdtempSync(join(tmpdir(), 'hover-list-specs-'));
  });

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

  test('returns empty list when __vibe_tests__/ does not exist', async () => {
    const specs = await listSpecs(tmp);
    expect(specs).toEqual([]);
  });

  test('lists *.spec.ts files in __vibe_tests__/ with parsed header', async () => {
    mkdirSync(join(tmp, '__vibe_tests__'));
    writeFileSync(
      join(tmp, '__vibe_tests__', 'login.spec.ts'),
      [
        `/**`,
        ` * Original prompt: log in as alice`,
        ` * Outcome: ok.`,
        ` * Steps:`,
        ` *   1. step one`,
        ` *   2. step two`,
        ` */`,
        `test('login', async () => {});`,
      ].join('\n'),
    );

    const specs = await listSpecs(tmp);
    expect(specs.length).toBe(1);
    expect(specs[0].slug).toBe('login');
    expect(specs[0].originalPrompt).toBe('log in as alice');
    expect(specs[0].outcome).toBe('ok.');
    expect(specs[0].stepCount).toBe(2);
    expect(specs[0].path.endsWith('login.spec.ts')).toBe(true);
  });

  test('reports originalPrompt=null for hand-authored specs without a Hover header', async () => {
    mkdirSync(join(tmp, '__vibe_tests__'));
    writeFileSync(
      join(tmp, '__vibe_tests__', 'hand-written.spec.ts'),
      `import { test } from '@playwright/test';\ntest('x', async () => {});\n`,
    );
    const specs = await listSpecs(tmp);
    expect(specs[0].originalPrompt).toBeNull();
    expect(specs[0].stepCount).toBe(0);
  });

  test('skips non-.spec.ts files', async () => {
    mkdirSync(join(tmp, '__vibe_tests__'));
    writeFileSync(join(tmp, '__vibe_tests__', 'a.spec.ts'), `test('a', async () => {});`);
    writeFileSync(join(tmp, '__vibe_tests__', 'README.md'), `notes`);
    writeFileSync(join(tmp, '__vibe_tests__', 'fixtures.ts'), `export {};`);

    const specs = await listSpecs(tmp);
    expect(specs.map((s) => s.slug)).toEqual(['a']);
  });

  test('sorts newest-first by mtime', async () => {
    mkdirSync(join(tmp, '__vibe_tests__'));
    const older = join(tmp, '__vibe_tests__', 'older.spec.ts');
    const newer = join(tmp, '__vibe_tests__', 'newer.spec.ts');
    writeFileSync(older, `test('older', async () => {});`);
    await new Promise((r) => setTimeout(r, 30)); // ensure distinct mtimes
    writeFileSync(newer, `test('newer', async () => {});`);

    const specs = await listSpecs(tmp);
    expect(specs.map((s) => s.slug)).toEqual(['newer', 'older']);
  });

  test('reports optimizableCount parsed from the spec body', async () => {
    mkdirSync(join(tmp, '__vibe_tests__'));
    writeFileSync(
      join(tmp, '__vibe_tests__', 'marked.spec.ts'),
      [
        `test('a', async () => {`,
        `  // hover:optimizable: browser_file_upload — ...`,
        `  await page.goto('/');`,
        `});`,
      ].join('\n'),
    );
    writeFileSync(join(tmp, '__vibe_tests__', 'clean.spec.ts'), `test('b', async () => {});`);

    const specs = await listSpecs(tmp);
    const counts = Object.fromEntries(specs.map((s) => [s.slug, s.optimizableCount]));
    expect(counts['marked']).toBe(1);
    expect(counts['clean']).toBe(0);
  });

  test('attaches an optimization suggestion — markers + sidecar → suggested', async () => {
    mkdirSync(join(tmp, '__vibe_tests__', '.hover'), { recursive: true });
    writeFileSync(
      join(tmp, '__vibe_tests__', 'marked.spec.ts'),
      [
        `test('a', async () => {`,
        `  // hover:optimizable: browser_file_upload — ...`,
        `  await page.goto('/');`,
        `});`,
      ].join('\n'),
    );
    writeFileSync(
      join(tmp, '__vibe_tests__', '.hover', 'marked.json'),
      JSON.stringify({ steps: [{ kind: 'step', tool: 'browser_file_upload' }] }),
    );
    // No marker and no sidecar → not suggested.
    writeFileSync(join(tmp, '__vibe_tests__', 'plain.spec.ts'), `test('b', async () => {});`);

    const specs = await listSpecs(tmp);
    const byslug = Object.fromEntries(specs.map((s) => [s.slug, s]));
    expect(byslug['marked'].optimization.suggested).toBe(true);
    expect(byslug['marked'].optimization.reasons[0]).toContain('1 interaction');
    expect(byslug['plain'].optimization.suggested).toBe(false);
  });

  test('optimization names a relevant seed when the sidecar tools match', async () => {
    mkdirSync(join(tmp, '__vibe_tests__', '.hover'), { recursive: true });
    writeFileSync(join(tmp, '__vibe_tests__', 'dl.spec.ts'), `test('c', async () => {});`);
    // built-in `download` seed has signature ['browser_click'] → relevant here.
    writeFileSync(
      join(tmp, '__vibe_tests__', '.hover', 'dl.json'),
      JSON.stringify({ steps: [{ kind: 'step', tool: 'browser_click' }] }),
    );

    const specs = await listSpecs(tmp);
    const dl = specs.find((s) => s.slug === 'dl')!;
    expect(dl.optimization.suggested).toBe(true);
    expect(dl.optimization.reasons.join(' ')).toContain('download');
  });

  test('reports hasSidecar — true when .hover/<slug>.json exists, false otherwise', async () => {
    mkdirSync(join(tmp, '__vibe_tests__', '.hover'), { recursive: true });
    writeFileSync(join(tmp, '__vibe_tests__', 'with.spec.ts'), `test('a', async () => {});`);
    writeFileSync(join(tmp, '__vibe_tests__', '.hover', 'with.json'), '{}');
    writeFileSync(join(tmp, '__vibe_tests__', 'without.spec.ts'), `test('b', async () => {});`);

    const specs = await listSpecs(tmp);
    const sidecar = Object.fromEntries(specs.map((s) => [s.slug, s.hasSidecar]));
    expect(sidecar['with']).toBe(true);
    expect(sidecar['without']).toBe(false);
  });
});
