import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import type { BrowserSessionRepo } from '../db/browser-session-repo.js';

vi.mock('./script-runner.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./script-runner.js')>();
  return { ...actual, runUserScript: vi.fn() };
});
vi.mock('./session-loader.js', () => ({ loadSessionStateForUser: vi.fn() }));

import { runUserScript } from './script-runner.js';
import { loadSessionStateForUser } from './session-loader.js';
import { resolveScriptForKind, resolveAndRunUserScript } from './script-orchestrator.js';

const USER = 'user-1';
let root: string;

function addMacro(name: string, source = 'export default async () => 1;\n'): string {
  const dir = join(root, USER, 'browser-macros');
  mkdirSync(dir, { recursive: true });
  const p = join(dir, name);
  writeFileSync(p, source);
  return p;
}

const MACRO_WITH_SESSION = `---
description: needs a session
session_profile_id: 7
---
export default async () => 'macro';
`;

beforeEach(() => {
  root = mkdtempSync(join(tmpdir(), 'script-orch-test-'));
  vi.clearAllMocks();
  vi.mocked(runUserScript).mockResolvedValue({ result: 'ran', logs: ['l1'], durationMs: 12 } as never);
});

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

describe('resolveScriptForKind', () => {
  it('resolves a browser-macro to its path with the playwright runtime', () => {
    const p = addMacro('macro.js');
    const r = resolveScriptForKind(root, USER, 'macro.js', 'browser-macro');
    expect(r).toEqual({ scriptPath: p, subdir: 'browser-macros', runtime: 'playwright' });
  });

  it('resolves without an explicit kind', () => {
    const p = addMacro('macro.js');
    const r = resolveScriptForKind(root, USER, 'macro.js', undefined);
    expect(r).toMatchObject({ scriptPath: p, subdir: 'browser-macros' });
  });

  it('returns an error for a missing macro', () => {
    const r = resolveScriptForKind(root, USER, 'ghost.js', undefined);
    expect(r).toEqual({ error: 'browser-macro not found: browser-macros/ghost.js' });
  });

  it('treats traversal names as not found instead of escaping', () => {
    const r = resolveScriptForKind(root, USER, '../../etc/passwd', undefined);
    expect('error' in r).toBe(true);
  });
});

describe('resolveAndRunUserScript', () => {
  it('runs a macro and appends .js to the name', async () => {
    const p = addMacro('task.js');
    const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'task', params: { a: 1 } });
    expect(r).toMatchObject({
      ok: true, result: 'ran', logs: ['l1'], subdir: 'browser-macros', runtime: 'playwright',
    });
    expect(runUserScript).toHaveBeenCalledWith(
      expect.objectContaining({
        scriptPath: p, params: { a: 1 }, runtime: 'playwright', timeoutMs: 60_000,
      }),
    );
    expect(loadSessionStateForUser).not.toHaveBeenCalled();
  });

  it('returns an error result for an unknown macro', async () => {
    const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'ghost', params: {} });
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.error).toContain('not found');
    expect(runUserScript).not.toHaveBeenCalled();
  });

  it('runs a macro without a session profile directly (no storageState)', async () => {
    addMacro('macro.js');
    const r = await resolveAndRunUserScript({
      rootDir: root, userId: USER, name: 'macro', params: {}, headless: false,
    });
    expect(r).toMatchObject({ ok: true, subdir: 'browser-macros', runtime: 'playwright' });
    expect(runUserScript).toHaveBeenCalledWith(
      expect.objectContaining({ storageState: undefined, headless: false }),
    );
  });

  it('fails when a macro declares a session but no repo is configured', async () => {
    addMacro('macro.js', MACRO_WITH_SESSION);
    const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'macro', params: {} });
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.error).toContain('BrowserSessionRepo not configured');
    expect(runUserScript).not.toHaveBeenCalled();
  });

  it('propagates session load failures', async () => {
    addMacro('macro.js', MACRO_WITH_SESSION);
    vi.mocked(loadSessionStateForUser).mockResolvedValue({
      ok: false,
      error: { kind: 'profile_not_found', message: 'profile 7 not found' },
    } as never);
    const r = await resolveAndRunUserScript({
      rootDir: root, userId: USER, name: 'macro', params: {},
      sessRepo: {} as BrowserSessionRepo, masterKeyPath: '/k',
    });
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.error).toBe('profile 7 not found');
  });

  it('hydrates storageState and passes it to the runner', async () => {
    addMacro('macro.js', MACRO_WITH_SESSION);
    const state = { cookies: [] };
    vi.mocked(loadSessionStateForUser).mockResolvedValue({ ok: true, storageState: state } as never);
    const r = await resolveAndRunUserScript({
      rootDir: root, userId: USER, name: 'macro', params: {},
      sessRepo: {} as BrowserSessionRepo, masterKeyPath: '/k', timeoutMs: 5000,
    });
    expect(r.ok).toBe(true);
    expect(loadSessionStateForUser).toHaveBeenCalledWith(
      { sessRepo: {}, masterKeyPath: '/k' }, USER, 7,
    );
    expect(runUserScript).toHaveBeenCalledWith(
      expect.objectContaining({ storageState: state, timeoutMs: 5000 }),
    );
  });

  it('reports a frontmatter parse failure', async () => {
    addMacro('macro.js', '---\nsession_profile_id: -2\n---\nbody');
    const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'macro', params: {} });
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.error).toContain('failed to parse script frontmatter');
  });

  it('never throws when the runner fails — returns ok:false with duration', async () => {
    addMacro('boom.js');
    vi.mocked(runUserScript).mockRejectedValue(new Error('script exploded'));
    const r = await resolveAndRunUserScript({ rootDir: root, userId: USER, name: 'boom', params: {} });
    expect(r.ok).toBe(false);
    if (!r.ok) {
      expect(r.error).toBe('script exploded');
      expect(typeof r.durationMs).toBe('number');
    }
  });
});
