/**
 * app-test.test.ts — pure-function unit tests for TestWorkspaceApp helpers.
 * These cover buildHarnessUrl, validateInput, and evaluateExpectations without
 * any network/browser calls.
 */
import { describe, it, expect } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { buildHarnessUrl, validateInput, evaluateExpectations } from './app-test.js';

describe('buildHarnessUrl', () => {
  it('assembles a localhost URL with space, app, token, and default entry', () => {
    const url = buildHarnessUrl(9876, {
      space: 'space-abc',
      app: 'my-app',
      entry: 'apps/my-app/index.html',
      token: 'tok123',
    });
    expect(url).toBe(
      'http://localhost:9876/app-harness?space=space-abc&app=my-app&entry=apps%2Fmy-app%2Findex.html&token=tok123',
    );
  });

  it('URL-encodes special characters in entry', () => {
    const url = buildHarnessUrl(3000, {
      space: 's1',
      app: 'calc',
      entry: 'apps/calc/index.html',
      token: 'abc',
    });
    expect(url).toContain('entry=apps%2Fcalc%2Findex.html');
  });

  it('uses the supplied port', () => {
    const url = buildHarnessUrl(4242, { space: 's', app: 'a', entry: 'apps/a/index.html', token: 't' });
    expect(url).toMatch(/^http:\/\/localhost:4242\//);
  });
});

describe('validateInput', () => {
  it('rejects when app is missing', () => {
    expect(() => validateInput({ space: 's1' })).toThrow(/app/);
  });

  it('rejects when space is missing', () => {
    expect(() => validateInput({ app: 'my-app' })).toThrow(/space/);
  });

  it('defaults entry to apps/{app}/index.html when omitted', () => {
    const result = validateInput({ space: 's1', app: 'counter' });
    expect(result.entry).toBe('apps/counter/index.html');
  });

  it('preserves an explicitly supplied entry', () => {
    const result = validateInput({ space: 's1', app: 'counter', entry: 'apps/counter/v2.html' });
    expect(result.entry).toBe('apps/counter/v2.html');
  });

  it('passes through steps and seed_files (canonical param name)', () => {
    const steps = [{ type: 'click', selector: '#btn' }];
    const seed = [{ path: 'apps/counter/index.html', content: '<html/>' }];
    const result = validateInput({ space: 's1', app: 'counter', steps, seed_files: seed });
    expect(result.steps).toStrictEqual(steps);
    expect(result.seed_files).toStrictEqual(seed);
  });

  it('accepts legacy "actions" param and maps to steps', () => {
    const actions = [{ type: 'click', selector: '#btn' }];
    const result = validateInput({ space: 's1', app: 'counter', actions });
    expect(result.steps).toStrictEqual(actions);
  });

  it('parses canonical expect items (kind text and file)', () => {
    const raw = {
      space: 's1',
      app: 'counter',
      expect: [
        { kind: 'text', target: '#status', contains: 'OK' },
        { kind: 'file', target: 'output/result.txt', contains: 'done' },
      ],
    };
    const result = validateInput(raw);
    expect(result.expect).toHaveLength(2);
    expect(result.expect[0]).toEqual({ kind: 'text', target: '#status', contains: 'OK' });
    expect(result.expect[1]).toEqual({ kind: 'file', target: 'output/result.txt', contains: 'done' });
  });
});

describe('evaluateExpectations', () => {
  // ---------------------------------------------------------------------------
  // kind: "text" — success / failure
  // ---------------------------------------------------------------------------
  it('kind=text passes when contains is present in pageText', () => {
    const failures = evaluateExpectations(
      [{ kind: 'text', target: '#status', contains: '保存OK' }],
      { pageText: 'some text 保存OK more text', workspacePath: '/tmp' },
    );
    expect(failures).toHaveLength(0);
  });

  it('kind=text records a failure when contains is ABSENT (no silent no-op)', () => {
    const failures = evaluateExpectations(
      [{ kind: 'text', target: '#status', contains: '保存OK' }],
      { pageText: 'nothing relevant here', workspacePath: '/tmp' },
    );
    expect(failures).toHaveLength(1);
    expect(failures[0]).toMatch(/保存OK/);
    expect(failures[0]).toMatch(/not found/);
  });

  it('kind=text with empty contains always passes (empty string is substring of everything)', () => {
    const failures = evaluateExpectations(
      [{ kind: 'text', target: 'body', contains: '' }],
      { pageText: 'anything', workspacePath: '/tmp' },
    );
    expect(failures).toHaveLength(0);
  });

  // ---------------------------------------------------------------------------
  // kind: "file" — success / failure
  // ---------------------------------------------------------------------------
  it('kind=file passes when file exists and contains the needle', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'app-test-'));
    const filePath = path.join(tmpDir, 'result.txt');
    fs.writeFileSync(filePath, 'hello world from test');

    const failures = evaluateExpectations(
      [{ kind: 'file', target: 'result.txt', contains: 'from test' }],
      { pageText: '', workspacePath: tmpDir },
    );
    expect(failures).toHaveLength(0);

    fs.rmSync(tmpDir, { recursive: true });
  });

  it('kind=file records failure when file exists but does not contain needle', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'app-test-'));
    const filePath = path.join(tmpDir, 'result.txt');
    fs.writeFileSync(filePath, 'hello world');

    const failures = evaluateExpectations(
      [{ kind: 'file', target: 'result.txt', contains: 'NOT_PRESENT' }],
      { pageText: '', workspacePath: tmpDir },
    );
    expect(failures).toHaveLength(1);
    expect(failures[0]).toMatch(/NOT_PRESENT/);

    fs.rmSync(tmpDir, { recursive: true });
  });

  it('kind=file records failure when file does not exist', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'app-test-'));

    const failures = evaluateExpectations(
      [{ kind: 'file', target: 'missing.txt', contains: 'anything' }],
      { pageText: '', workspacePath: tmpDir },
    );
    expect(failures).toHaveLength(1);
    expect(failures[0]).toMatch(/does not exist/);

    fs.rmSync(tmpDir, { recursive: true });
  });

  it('kind=file records failure when target is empty string', () => {
    const failures = evaluateExpectations(
      [{ kind: 'file', target: '', contains: 'x' }],
      { pageText: '', workspacePath: '/tmp' },
    );
    expect(failures).toHaveLength(1);
    expect(failures[0]).toMatch(/"target"/);
  });

  // ---------------------------------------------------------------------------
  // Unknown kind — must never silently pass
  // ---------------------------------------------------------------------------
  it('unknown kind records a failure (never silently passes)', () => {
    const failures = evaluateExpectations(
      [{ kind: 'element_visible', target: '#btn', contains: '' } as any],
      { pageText: 'anything', workspacePath: '/tmp' },
    );
    expect(failures).toHaveLength(1);
    expect(failures[0]).toMatch(/unknown kind/);
    expect(failures[0]).toMatch(/element_visible/);
  });

  it('another legacy kind "text_contains" also records a failure', () => {
    const failures = evaluateExpectations(
      [{ kind: 'text_contains', contains: 'hi' } as any],
      { pageText: 'hi there', workspacePath: '/tmp' },
    );
    expect(failures).toHaveLength(1);
    expect(failures[0]).toMatch(/unknown kind/);
  });

  // ---------------------------------------------------------------------------
  // Multiple expectations — independent evaluation
  // ---------------------------------------------------------------------------
  it('evaluates all expectations independently and collects all failures', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'app-test-'));
    fs.writeFileSync(path.join(tmpDir, 'out.txt'), 'correct content');

    const failures = evaluateExpectations(
      [
        { kind: 'text', target: 'body', contains: 'MISSING_TEXT' },
        { kind: 'file', target: 'out.txt', contains: 'correct content' },
        { kind: 'file', target: 'nonexistent.txt', contains: 'x' },
      ],
      { pageText: 'page has something else', workspacePath: tmpDir },
    );
    // First and third fail, second passes
    expect(failures).toHaveLength(2);

    fs.rmSync(tmpDir, { recursive: true });
  });
});
