import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { recorder } from '../engine/browser-recorder.js';
import { flushAndStageRecording } from './recording-flush.js';
import { parseScript } from './frontmatter.js';
import { logger } from '../logger.js';

// ── Fixtures ──────────────────────────────────────────────────────────────────

const SCRIPT_FOO = `\
---
description: Log into example.com
params:
  - name: username
    type: string
created_at: '2026-01-01T00:00:00.000Z'
---
async function main({ context, params }) {
  const page = await context.newPage();
  try {
    await page.goto('https://example.com');
  } finally {
    await page.close();
  }
}
module.exports = main;
`;

const TEST_USER = 'flush-test-user';

// ── Setup ─────────────────────────────────────────────────────────────────────

let tempDir: string;
let userFolderRoot: string;

beforeEach(() => {
  tempDir = mkdtempSync(join(tmpdir(), 'recording-flush-test-'));
  userFolderRoot = join(tempDir, 'users');
  mkdirSync(join(userFolderRoot, TEST_USER, 'browser-macros'), { recursive: true });
  mkdirSync(join(userFolderRoot, TEST_USER, 'recordings'), { recursive: true });
});

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

// ── Helpers ───────────────────────────────────────────────────────────────────

function makeTaskId(): string {
  return `flush-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}

function recordSomeActions(taskId: string): void {
  recorder.record(taskId, { type: 'goto', url: 'https://example.com' });
  recorder.record(taskId, { type: 'click', selector: '#btn' });
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('flushAndStageRecording', () => {
  it('flushes a regular recording without staging a patch', () => {
    const taskId = makeTaskId();
    recorder.enable(taskId, 'my-rec');
    recordSomeActions(taskId);

    flushAndStageRecording(taskId, TEST_USER, userFolderRoot);

    // Recording file should exist
    const recPath = join(userFolderRoot, TEST_USER, 'recordings', 'my-rec.json');
    expect(existsSync(recPath)).toBe(true);

    // Patch file should NOT exist (no .next suffix)
    const patchPath = join(userFolderRoot, TEST_USER, 'browser-macros', 'my-rec.next.js');
    expect(existsSync(patchPath)).toBe(false);
  });

  it('is a no-op when ownerId is undefined', () => {
    const taskId = makeTaskId();
    recorder.enable(taskId, 'my-rec');
    recordSomeActions(taskId);

    // Should not throw
    expect(() => flushAndStageRecording(taskId, undefined, userFolderRoot)).not.toThrow();

    // Nothing should have been flushed (recorder still holds the buffer)
    // Actually the buffer is not flushed since we returned early
    recorder.cancel(taskId); // clean up
  });

  it('is a no-op when recorder is not enabled for the taskId', () => {
    const taskId = makeTaskId();
    // No recorder.enable call
    expect(() => flushAndStageRecording(taskId, TEST_USER, userFolderRoot)).not.toThrow();
  });

  it('stages a .next.js patch in browser-macros/ when recordTo ends in .next', () => {
    const taskId = makeTaskId();

    // Write source script in browser-macros/
    writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'foo.js'), SCRIPT_FOO);

    recorder.enable(taskId, 'foo.next');
    recordSomeActions(taskId);

    flushAndStageRecording(taskId, TEST_USER, userFolderRoot);

    // Recording JSON should exist
    const recPath = join(userFolderRoot, TEST_USER, 'recordings', 'foo.next.json');
    expect(existsSync(recPath)).toBe(true);

    // Patch script should exist in browser-macros/
    const patchPath = join(userFolderRoot, TEST_USER, 'browser-macros', 'foo.next.js');
    expect(existsSync(patchPath)).toBe(true);

    // Parse the patch and verify description is preserved from foo.js
    const patchText = readFileSync(patchPath, 'utf-8');
    const parsed = parseScript(patchText);
    expect(parsed.frontmatter.description).toBe('Log into example.com');

    // createdAt should be preserved from the original script
    expect(parsed.frontmatter.createdAt).toBe('2026-01-01T00:00:00.000Z');

    // updatedAt should be a fresh ISO timestamp
    expect(parsed.frontmatter.updatedAt).toBeDefined();
    expect(parsed.frontmatter.updatedAt).not.toBe('2026-01-01T00:00:00.000Z');
    expect(() => new Date(parsed.frontmatter.updatedAt!)).not.toThrow();
  });

  it('logs a warning if the source script is missing and still flushes the recording', () => {
    const taskId = makeTaskId();
    const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});

    recorder.enable(taskId, 'phantom.next');
    recordSomeActions(taskId);

    flushAndStageRecording(taskId, TEST_USER, userFolderRoot);

    // Recording should still be flushed
    const recPath = join(userFolderRoot, TEST_USER, 'recordings', 'phantom.next.json');
    expect(existsSync(recPath)).toBe(true);

    // Patch should NOT exist (no source script in browser-macros/)
    const patchPath = join(userFolderRoot, TEST_USER, 'browser-macros', 'phantom.next.js');
    expect(existsSync(patchPath)).toBe(false);

    // logger.warn should have been called with a message about the source script
    const warnCalls = warnSpy.mock.calls.map(c => c[0] as string);
    expect(warnCalls.some(msg => msg.includes('source script'))).toBe(true);

    warnSpy.mockRestore();
  });

  it('does not flush when the buffer is empty', () => {
    const taskId = makeTaskId();
    recorder.enable(taskId, 'empty-rec');
    // No actions recorded

    flushAndStageRecording(taskId, TEST_USER, userFolderRoot);

    const recPath = join(userFolderRoot, TEST_USER, 'recordings', 'empty-rec.json');
    expect(existsSync(recPath)).toBe(false);
  });

  it('does not substitute empty-string fill values with params on a .next.js patch', () => {
    const taskId = makeTaskId();

    // Write source script in browser-macros/
    writeFileSync(join(userFolderRoot, TEST_USER, 'browser-macros', 'foo.js'), SCRIPT_FOO);

    recorder.enable(taskId, 'foo.next');
    // Record an empty-string fill action (e.g., clearing a checkbox)
    recorder.record(taskId, { type: 'fill', selector: '#empty-field', value: '', frameChain: [] });

    flushAndStageRecording(taskId, TEST_USER, userFolderRoot);

    // Patch script should exist in browser-macros/
    const patchPath = join(userFolderRoot, TEST_USER, 'browser-macros', 'foo.next.js');
    expect(existsSync(patchPath)).toBe(true);

    // Parse and verify the patch contains literal empty-string fill, not params.username
    const patchText = readFileSync(patchPath, 'utf-8');
    expect(patchText).toContain('.fill("")');
    expect(patchText).not.toContain('params.username');
  });
});
