import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
import { join } from 'path';
import { compileScript } from './script-compiler.js';
import type { CompileScriptOptions } from './script-compiler.js';
import type { RecordedAction } from '../engine/browser-recorder.js';

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

function action(
  type: RecordedAction['type'],
  extras: Partial<Omit<RecordedAction, 'type' | 'ts'>> = {}
): RecordedAction {
  return { type, ts: '2026-05-09T12:00:00Z', ...extras };
}

// ── tests ─────────────────────────────────────────────────────────────────────

describe('user-folder/script-compiler', () => {
  // Test 1: Empty recording
  it('produces a wrapper with no actions and returns undefined when recording is empty', () => {
    const { source, meta } = compileScript({
      recording: [],
      description: 'Empty script',
    });
    expect(meta.description).toBe('Empty script');
    expect(meta.params).toEqual([]);
    // Body must contain try/finally wrapper
    expect(source).toContain('async function main({ context, params })');
    expect(source).toContain('const page = await context.newPage();');
    expect(source).toContain('await page.close();');
    // No return statement (undefined return)
    expect(source).not.toContain('return __text');
    expect(source).not.toContain('return __html');
  });

  // Test 2: Single goto
  it('generates await page.goto for a goto action', () => {
    const { source } = compileScript({
      recording: [action('goto', { url: 'https://example.com' })],
      description: 'Goto test',
    });
    expect(source).toContain("await page.goto('https://example.com');");
  });

  // Test 3: Click with stable selector
  it('generates a locator click preserving the selector literally', () => {
    const { source } = compileScript({
      recording: [action('click', { selector: 'button[data-testid="submit"]' })],
      description: 'Click test',
    });
    expect(source).toContain("await page.locator('button[data-testid=\"submit\"]').click();");
  });

  // Test 4: Fill with paramHint match
  it('replaces fill value with params.{name} when paramHint matches', () => {
    const { source, meta } = compileScript({
      recording: [action('fill', { selector: '#username', value: 'admin' })],
      description: 'Fill param test',
      paramHints: [{ name: 'username', valueToReplace: 'admin', type: 'string' }],
    });
    expect(source).toContain("await page.locator('#username').fill(params.username);");
    expect(meta.params).toEqual([{ name: 'username', type: 'string' }]);
  });

  // Test 5: Fill without paramHint match emits literal string
  it('emits the literal string when no paramHint matches the fill value', () => {
    const { source, meta } = compileScript({
      recording: [action('fill', { selector: '#notes', value: 'hello world' })],
      description: 'Fill literal test',
    });
    expect(source).toContain('await page.locator(\'#notes\').fill("hello world");');
    expect(meta.params).toEqual([]);
  });

  // Test 6: Multiple fills with the same value — both use params.{name}
  it('substitutes params.{name} in ALL fill calls sharing the same valueToReplace', () => {
    const { source } = compileScript({
      recording: [
        action('fill', { selector: '#a', value: 'mytoken' }),
        action('fill', { selector: '#b', value: 'mytoken' }),
      ],
      description: 'Multiple fill test',
      paramHints: [{ name: 'token', valueToReplace: 'mytoken', type: 'string' }],
    });
    expect(source).toContain("await page.locator('#a').fill(params.token);");
    expect(source).toContain("await page.locator('#b').fill(params.token);");
  });

  // Test 7: Last action is getText — script returns __text
  it('returns __text when the last action is getText', () => {
    const { source } = compileScript({
      recording: [
        action('goto', { url: 'https://example.com' }),
        action('getText', { selector: 'h1' }),
      ],
      description: 'getText return test',
    });
    expect(source).toContain(
      "const __text = await page.locator('h1').textContent();"
    );
    expect(source).toContain('return __text;');
  });

  // Test 8: Last action is dumpHtml — script returns __html
  it('returns __html when the last action is dumpHtml', () => {
    const { source } = compileScript({
      recording: [action('dumpHtml', { selector: '#root' })],
      description: 'dumpHtml return test',
    });
    expect(source).toContain(
      "const __html = await page.locator('#root').evaluate(el => el.outerHTML);"
    );
    expect(source).toContain('return __html;');
  });

  // Test 9: Snapshot test — 5-action recording
  it('snapshot: 5-action recording matches expected-compiled.js fixture', () => {
    const fixturePath = join(
      new URL('.', import.meta.url).pathname,
      '../../tests/fixtures/scripts/expected-compiled.js'
    );
    const expected = readFileSync(fixturePath, 'utf-8');

    const opts: CompileScriptOptions = {
      recording: [
        action('goto', { url: 'https://example.com/login' }),
        action('fill', { selector: '[data-testid="username"]', value: 'admin' }),
        action('fill', { selector: '[data-testid="password"]', value: 'secret' }),
        action('click', { selector: 'button[type="submit"]' }),
        action('getText', { selector: '[data-testid="dashboard"]' }),
      ],
      description: 'Log in and grab the dashboard table',
      sessionProfileId: 7,
      recordingSource: 'rec-test.json',
      paramHints: [
        { name: 'user', valueToReplace: 'admin', type: 'string' },
        { name: 'pass', valueToReplace: 'secret', type: 'string' },
      ],
    };

    const { source } = compileScript(opts);
    expect(source).toBe(expected);
  });

  // Test 10a: frameChain with unique attr selectors → frameLocator chain
  it('compiles a frameChain of unique selectors to a frameLocator chain', () => {
    const { source } = compileScript({
      recording: [
        action('click', {
          selector: '#inner-btn',
          frameChain: [
            { selector: 'iframe[name="checkout"]' },
            { selector: 'iframe[id="card"]' },
          ],
        }),
      ],
      description: 'iframe click test',
    });
    expect(source).toContain(
      `await page.frameLocator('iframe[name="checkout"]').frameLocator('iframe[id="card"]').locator('#inner-btn').click();`
    );
    expect(source).not.toContain('// TODO: iframe');
  });

  // Test 10b: frameChain with positional fallback → locator().nth().contentFrame()
  it('compiles a positional frameChain entry to locator().nth().contentFrame()', () => {
    const { source } = compileScript({
      recording: [
        action('fill', {
          selector: 'input.card',
          value: '4242',
          frameChain: [{ selector: 'iframe', index: 1 }],
        }),
      ],
      description: 'positional iframe fill',
    });
    expect(source).toContain(
      `await page.locator('iframe').nth(1).contentFrame().locator('input.card').fill("4242");`
    );
  });

  // Test 10c: legacy string[] frameChain (backwards compat) → frameLocator chain
  it('accepts legacy string[] frameChain (backwards compat)', () => {
    const { source } = compileScript({
      recording: [
        action('click', { selector: '.btn', frameChain: ['iframe[name="legacy"]'] }),
      ],
      description: 'legacy chain',
    });
    expect(source).toContain(
      `await page.frameLocator('iframe[name="legacy"]').locator('.btn').click();`
    );
  });

  // Test 10d: empty frameChain → unchanged page.locator(...)
  it('compiles empty frameChain identically to no chain', () => {
    const { source: withEmpty } = compileScript({
      recording: [action('click', { selector: '.btn', frameChain: [] })],
      description: 'empty',
    });
    const { source: withoutChain } = compileScript({
      recording: [action('click', { selector: '.btn' })],
      description: 'absent',
    });
    expect(withEmpty).toContain("await page.locator('.btn').click();");
    expect(withoutChain).toContain("await page.locator('.btn').click();");
  });

  // Test 10e: dumpHtml with frameChain → applied
  it('applies frameChain to dumpHtml', () => {
    const { source } = compileScript({
      recording: [
        action('dumpHtml', {
          selector: '.contents',
          frameChain: [{ selector: 'iframe[name="cart"]' }],
        }),
      ],
      description: 'iframe dumpHtml',
    });
    expect(source).toContain(
      `await page.frameLocator('iframe[name="cart"]').locator('.contents').evaluate(el => el.outerHTML);`
    );
  });

  // Test 11: Only USED paramHints appear in frontmatter params (unused ones are dropped)
  it('drops unused paramHints from frontmatter params', () => {
    const { source, meta } = compileScript({
      recording: [action('fill', { selector: '#q', value: 'hello' })],
      description: 'Unused hint test',
      paramHints: [
        { name: 'query', valueToReplace: 'hello', type: 'string' },
        { name: 'unused', valueToReplace: 'never-used', type: 'number' },
      ],
    });
    expect(meta.params).toHaveLength(1);
    expect(meta.params[0].name).toBe('query');
    expect(source).not.toContain('unused');
  });

  // Test 12: session_profile_id and recording_source round-trip
  it('includes session_profile_id and recording_source in frontmatter when provided, omits them when not', () => {
    const withBoth = compileScript({
      recording: [],
      description: 'Round-trip test',
      sessionProfileId: 42,
      recordingSource: 'rec-2026-05-09T12-34-56.json',
    });
    expect(withBoth.source).toContain('session_profile_id: 42');
    expect(withBoth.source).toContain('recording_source: rec-2026-05-09T12-34-56.json');
    expect(withBoth.meta.sessionProfileId).toBe(42);
    expect(withBoth.meta.recordingSource).toBe('rec-2026-05-09T12-34-56.json');

    const withoutBoth = compileScript({
      recording: [],
      description: 'No optional fields',
    });
    expect(withoutBoth.source).not.toContain('session_profile_id');
    expect(withoutBoth.source).not.toContain('recording_source');
    expect(withoutBoth.meta.sessionProfileId).toBeUndefined();
    expect(withoutBoth.meta.recordingSource).toBeUndefined();
  });
});
