import assert from 'node:assert/strict';
import test from 'node:test';
import { parseConfig } from '../src/config.js';
import { X11Backend, parseXrandrListMonitors, virtualBounds } from '../src/backend/x11.js';
import type { CommandRunner, RunOptions, RunResult } from '../src/runner.js';

const topology = `Monitors: 3\n 0: +*HDMI-1 1920/597x1080/336+0+0  HDMI-1\n 1: +eDP-1 1280/301x800/188+3840+0  eDP-1\n 2: +DP-3 1920/528x1080/297+1920+0  DP-3\n`;

class FakeRunner implements CommandRunner {
  readonly calls: Array<{ command: string; args: readonly string[]; options?: RunOptions }> = [];
  constructor(readonly screenshot = Buffer.from([0x89, 0x50, 0x4e, 0x47])) {}
  async run(command: string, args: readonly string[], options?: RunOptions): Promise<RunResult> {
    this.calls.push({ command, args: [...args], ...(options === undefined ? {} : { options }) });
    if (command === 'xrandr') return { exitCode: 0, stdout: Buffer.from(topology), stderr: Buffer.alloc(0), timedOut: false };
    if (command === 'ffmpeg') return { exitCode: 0, stdout: this.screenshot, stderr: Buffer.alloc(0), timedOut: false };
    if (command === 'xdotool') return { exitCode: 0, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), timedOut: false };
    throw new Error(`unexpected command ${command}`);
  }
}

test('xrandr monitor parser captures topology and primary display', () => {
  const displays = parseXrandrListMonitors(topology);
  assert.equal(displays.length, 3);
  assert.deepEqual(displays[0], { id: 'HDMI-1', name: 'HDMI-1', x: 0, y: 0, width: 1920, height: 1080, primary: true, pixelWidth: 1920, pixelHeight: 1080, scale: 1 });
  assert.deepEqual(virtualBounds(displays), { x: 0, y: 0, width: 5120, height: 1080 });
});

test('observe display uses resolved X11 region and returns raw PNG bytes', async () => {
  const runner = new FakeRunner();
  const backend = new X11Backend(parseConfig({}), runner, { DISPLAY: ':0', XDG_SESSION_TYPE: 'x11' });
  const result = await backend.observe({ target: { kind: 'display', displayId: 'DP-3' }, includePointer: false });
  assert.equal(result.bytes, 4);
  assert.deepEqual(result.region, { x: 1920, y: 0, width: 1920, height: 1080 });
  const ffmpeg = runner.calls.find(call => call.command === 'ffmpeg');
  assert.ok(ffmpeg);
  assert.deepEqual(ffmpeg.args, [
    '-hide_banner', '-loglevel', 'error', '-f', 'x11grab', '-draw_mouse', '0', '-video_size', '1920x1080',
    '-i', ':0+1920,0', '-frames:v', '1', '-f', 'image2pipe', '-vcodec', 'png', 'pipe:1',
  ]);
});

test('display-relative click is translated but xdotool stays internal', async () => {
  const runner = new FakeRunner();
  const backend = new X11Backend(parseConfig({}), runner, { DISPLAY: ':0', XDG_SESSION_TYPE: 'x11' });
  await backend.act({ type: 'click', button: 'left', point: { space: 'display', displayId: 'DP-3', x: 10, y: 20 } });
  const call = runner.calls.find(candidate => candidate.command === 'xdotool');
  assert.ok(call);
  assert.deepEqual(call.args, ['mousemove', '--sync', '1930', '20', 'click', '1']);
});

test('drag performs down/move/up in one explicit backend execution', async () => {
  const runner = new FakeRunner();
  const backend = new X11Backend(parseConfig({}), runner, { DISPLAY: ':0', XDG_SESSION_TYPE: 'x11' });
  await backend.act({
    type: 'drag', button: 'left',
    from: { space: 'desktop', x: 100, y: 100 },
    to: { space: 'desktop', x: 200, y: 250 },
  });
  const calls = runner.calls.filter(candidate => candidate.command === 'xdotool');
  assert.equal(calls.length, 1);
  assert.deepEqual(calls[0]?.args, ['mousemove', '--sync', '100', '100', 'mousedown', '1', 'mousemove', '--sync', '200', '250', 'mouseup', '1']);
});

test('scroll bound is enforced before input execution', async () => {
  const runner = new FakeRunner();
  const backend = new X11Backend(parseConfig({ action: { maxScrollSteps: 3 } }), runner, { DISPLAY: ':0', XDG_SESSION_TYPE: 'x11' });
  await assert.rejects(() => backend.act({ type: 'scroll', deltaX: 0, deltaY: 4 }), /maximum steps/);
  assert.equal(runner.calls.some(call => call.command === 'xdotool'), false);
});

test('remaining X11 action variants map to internal xdotool commands', async () => {
  const cases = [
    {
      action: { type: 'move', point: { space: 'desktop', x: 10, y: 20 } } as const,
      args: ['mousemove', '--sync', '10', '20'],
    },
    {
      action: { type: 'doubleClick', button: 'right', intervalMs: 140 } as const,
      args: ['click', '--repeat', '2', '--delay', '140', '3'],
    },
    {
      action: { type: 'scroll', deltaX: -2, deltaY: 3 } as const,
      args: ['click', '--repeat', '3', '5', 'click', '--repeat', '2', '6'],
    },
    {
      action: { type: 'mouseDown', button: 'middle' } as const,
      args: ['mousedown', '2'],
    },
    {
      action: { type: 'mouseUp', button: 'middle' } as const,
      args: ['mouseup', '2'],
    },
    {
      action: { type: 'key', keys: ['ctrl+l', 'Return'] } as const,
      args: ['key', '--clearmodifiers', 'ctrl+l', 'Return'],
    },
    {
      action: { type: 'text', text: 'hello world', delayMs: 7 } as const,
      args: ['type', '--clearmodifiers', '--delay', '7', '--', 'hello world'],
    },
  ];

  for (const entry of cases) {
    const localRunner = new FakeRunner();
    const backend = new X11Backend(parseConfig({}), localRunner, { DISPLAY: ':0', XDG_SESSION_TYPE: 'x11' });
    await backend.act(entry.action);
    const call = localRunner.calls.find(candidate => candidate.command === 'xdotool');
    assert.ok(call);
    assert.deepEqual(call.args, entry.args);
  }
});

test('text byte limit is enforced before input execution', async () => {
  const runner = new FakeRunner();
  const backend = new X11Backend(parseConfig({ action: { maxTextBytes: 3 } }), runner, { DISPLAY: ':0', XDG_SESSION_TYPE: 'x11' });
  await assert.rejects(() => backend.act({ type: 'text', text: 'four', delayMs: 0 }), /byte limit/);
  assert.equal(runner.calls.some(call => call.command === 'xdotool'), false);
});
