/**
 * OSController tests — all native calls injected via `nutImpl`.
 *
 * Verifies:
 *   - capability gating (refuses ops the OS reports as unsupported)
 *   - coord / arg validation
 *   - op delegation to nut.js with the right shapes
 *   - error wrapping (raw → typed OSError codes)
 *   - lazy-load failure surfaces as NATIVE_UNAVAILABLE
 */

import { describe, test, expect, beforeEach, jest } from '@jest/globals';
import { createOSController, OSController, OSError } from '../osController.js';

// ─── nut.js test double ────────────────────────────────────────────────

function makeFakeNut(overrides = {}) {
  const calls = [];
  const record = (name) => (...args) => { calls.push({ name, args }); return Promise.resolve(); };

  // Minimal Point / Region / Button shims so the controller's `new Point(x,y)`
  // calls don't blow up.
  class Point  { constructor(x, y) { this.x = x; this.y = y; } }
  class Region { constructor(x, y, w, h) { Object.assign(this, { x, y, width: w, height: h }); } }
  const Button = { LEFT: 'L', RIGHT: 'R', MIDDLE: 'M' };
  // Real nut.js v4 exposes ONLY Left/Right modifier names — no plain
  // "Control"/"Shift". Reflect that here so the alias map gets the
  // same exercise as production. Plain "Control" → LeftControl via
  // KEY_ALIASES in osController.js.
  const Key = {
    LeftControl: 'CTRL', LeftShift: 'SHIFT', LeftAlt: 'ALT', LeftWin: 'WIN',
    S: 'S', F5: 'F5', Enter: 'ENTER', Escape: 'ESC',
  };

  const fakeImage = { toPng: async () => Buffer.from('png-bytes') };

  const nut = {
    screen: {
      grab:       jest.fn(async () => fakeImage),
      grabRegion: jest.fn(async () => fakeImage),
      width:      jest.fn(async () => 1920),
      height:     jest.fn(async () => 1080),
    },
    mouse: {
      move:          jest.fn(record('mouse.move')),
      click:         jest.fn(record('mouse.click')),
      doubleClick:   jest.fn(record('mouse.doubleClick')),
      pressButton:   jest.fn(record('mouse.pressButton')),
      releaseButton: jest.fn(record('mouse.releaseButton')),
      scrollDown:    jest.fn(record('mouse.scrollDown')),
      scrollUp:      jest.fn(record('mouse.scrollUp')),
    },
    keyboard: {
      config: { autoDelayMs: 0 },
      type:       jest.fn(record('keyboard.type')),
      pressKey:   jest.fn(record('keyboard.pressKey')),
      releaseKey: jest.fn(record('keyboard.releaseKey')),
    },
    getWindows: jest.fn(async () => []),
    Point, Region, Button, Key,
    _calls: calls,
    ...overrides,
  };
  return nut;
}

const FULL_CAPS = Object.freeze({
  os: 'win32', display: null,
  screenshot: true, mouseInput: true, keyboardInput: true,
  windowEnumeration: true, windowFocus: true,
  permissions: {}, degradedReason: null, probedAt: Date.now(),
});

const WAYLAND_CAPS = Object.freeze({
  os: 'linux', display: 'wayland',
  screenshot: true, mouseInput: false, keyboardInput: false,
  windowEnumeration: true, windowFocus: false,
  permissions: { portalsOnly: true },
  degradedReason: 'Wayland blocks input injection.',
  probedAt: Date.now(),
});

// ─── factory / lifecycle ───────────────────────────────────────────────

describe('createOSController', () => {
  test('returns an OSController instance', () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    expect(c).toBeInstanceOf(OSController);
  });
});

describe('lazy native loading', () => {
  test('describeCapabilities reports nativeLoaded=true after first op when nutImpl is injected', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    const info = await c.describeCapabilities();
    expect(info.nativeLoaded).toBe(true);
    expect(info.os).toBe('win32');
  });
});

// ─── screenshot ────────────────────────────────────────────────────────

describe('screenshot', () => {
  test('full-screen grab → png buffer', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    const buf = await c.screenshot();
    expect(Buffer.isBuffer(buf)).toBe(true);
    expect(nut.screen.grab).toHaveBeenCalledTimes(1);
    expect(nut.screen.grabRegion).not.toHaveBeenCalled();
  });

  test('region grab passes a Region with the right dims', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.screenshot({ region: { x: 10, y: 20, width: 100, height: 50 } });
    expect(nut.screen.grabRegion).toHaveBeenCalledTimes(1);
    const region = nut.screen.grabRegion.mock.calls[0][0];
    expect(region).toMatchObject({ x: 10, y: 20, width: 100, height: 50 });
  });

  test('invalid region → OS_INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.screenshot({ region: { x: -1, y: 0, width: 10, height: 10 } }))
      .rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });

  test('refuses when caps say screenshot is false', async () => {
    const caps = { ...FULL_CAPS, screenshot: false, degradedReason: 'no perms' };
    const c = createOSController({ capabilities: caps, nutImpl: makeFakeNut() });
    await expect(c.screenshot()).rejects.toMatchObject({ code: OSError.UNSUPPORTED });
  });
});

// ─── mouse ─────────────────────────────────────────────────────────────

describe('mouseMove / mouseClick', () => {
  test('mouseMove delegates to nut.mouse.move with a Point', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.mouseMove(100, 200);
    expect(nut.mouse.move).toHaveBeenCalledTimes(1);
    const arr = nut.mouse.move.mock.calls[0][0];
    expect(arr[0]).toMatchObject({ x: 100, y: 200 });
  });

  test('negative coords → INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.mouseMove(-1, 0)).rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });

  test('mouseClick single = move + click', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.mouseClick(50, 60);
    expect(nut.mouse.move).toHaveBeenCalledTimes(1);
    expect(nut.mouse.click).toHaveBeenCalledTimes(1);
    expect(nut.mouse.click).toHaveBeenCalledWith('L');
    expect(nut.mouse.doubleClick).not.toHaveBeenCalled();
  });

  test('mouseClick count=2 → doubleClick', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.mouseClick(50, 60, { count: 2, button: 'right' });
    expect(nut.mouse.doubleClick).toHaveBeenCalledWith('R');
  });

  test('unknown button → INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.mouseClick(1, 1, { button: 'sideways' }))
      .rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });

  test('mouseInput=false → UNSUPPORTED with remediation', async () => {
    const c = createOSController({ capabilities: WAYLAND_CAPS, nutImpl: makeFakeNut() });
    await expect(c.mouseClick(1, 1)).rejects.toMatchObject({
      code: OSError.UNSUPPORTED,
    });
  });
});

describe('mouseScroll', () => {
  test('dy > 0 → scrollDown', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.mouseScroll(10, 10, 3);
    expect(nut.mouse.scrollDown).toHaveBeenCalledWith(3);
  });
  test('dy < 0 → scrollUp', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.mouseScroll(10, 10, -5);
    expect(nut.mouse.scrollUp).toHaveBeenCalledWith(5);
  });
  test('dy non-finite → INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.mouseScroll(0, 0, NaN)).rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });
});

describe('mouseDrag', () => {
  test('press → move → release', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.mouseDrag({ x: 0, y: 0 }, { x: 100, y: 100 });
    expect(nut.mouse.pressButton).toHaveBeenCalledWith('L');
    expect(nut.mouse.releaseButton).toHaveBeenCalledWith('L');
    expect(nut.mouse.move).toHaveBeenCalledTimes(2);
  });
});

// ─── keyboard ──────────────────────────────────────────────────────────

describe('typeText / keyPress', () => {
  test('typeText forwards string and sets autoDelay', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.typeText('hello', { delayMs: 5 });
    expect(nut.keyboard.type).toHaveBeenCalledWith('hello');
    expect(nut.keyboard.config.autoDelayMs).toBe(5);
  });

  test('empty text → INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.typeText('')).rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });

  test('keyPress resolves chord through Key enum', async () => {
    const nut = makeFakeNut();
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.keyPress(['Control', 'S']);
    expect(nut.keyboard.pressKey).toHaveBeenCalledWith('CTRL', 'S');
    expect(nut.keyboard.releaseKey).toHaveBeenCalledWith('CTRL', 'S');
  });

  test('unknown key → INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.keyPress(['Bogus'])).rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });

  test('cross-platform aliases (Meta/Command/Cmd/Windows/Win) resolve to OS keys', async () => {
    // Build a fake nut.Key that includes the realistic names we want
    // aliases to resolve to. The alias map should normalise BEFORE the
    // strict Key[name] lookup so an agent's natural "Meta" works on
    // Windows even though nut.js calls it LeftWin.
    const nut = makeFakeNut({
      Key: { LeftWin: 'WIN', LeftCmd: 'CMD', LeftSuper: 'SUPER', LeftControl: 'CTRL', S: 'S' },
    });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await c.keyPress(['Meta']);
    expect(nut.keyboard.pressKey).toHaveBeenCalledWith('WIN');
    await c.keyPress(['Command']);
    expect(nut.keyboard.pressKey).toHaveBeenCalledWith('CMD');
    await c.keyPress(['Win', 'S']);
    expect(nut.keyboard.pressKey).toHaveBeenCalledWith('WIN', 'S');
  });

  test('error message hints at nearby key names', async () => {
    const nut = makeFakeNut({
      Key: { LeftWin: 'WIN', LeftCmd: 'CMD', LeftSuper: 'SUPER' },
    });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    try {
      await c.keyPress(['LeftWinx']);
      throw new Error('should have thrown');
    } catch (e) {
      expect(e.code).toBe(OSError.INVALID_INPUT);
      expect(e.message).toMatch(/did you mean.*LeftWin/);
    }
  });

  test('empty keys → INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.keyPress([])).rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });

  test('keyboardInput=false → UNSUPPORTED', async () => {
    const c = createOSController({ capabilities: WAYLAND_CAPS, nutImpl: makeFakeNut() });
    await expect(c.typeText('hi')).rejects.toMatchObject({ code: OSError.UNSUPPORTED });
  });
});

// ─── windows ───────────────────────────────────────────────────────────

describe('listWindows / focusWindow', () => {
  function fakeWin({ title, bounds }) {
    return {
      getTitle:  async () => title,
      getRegion: async () => bounds,
      focus:     jest.fn(async () => {}),
    };
  }

  test('listWindows returns titles + bounds', async () => {
    const w1 = fakeWin({ title: 'Editor', bounds: { x: 0, y: 0, width: 800, height: 600 } });
    const nut = makeFakeNut({ getWindows: jest.fn(async () => [w1]) });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    const list = await c.listWindows();
    expect(list).toHaveLength(1);
    expect(list[0]).toMatchObject({ title: 'Editor', bounds: { width: 800 } });
  });

  test('focusWindow matches by substring (case-insensitive)', async () => {
    const target = fakeWin({ title: 'My App — Untitled', bounds: {} });
    const other  = fakeWin({ title: 'Browser',           bounds: {} });
    const nut = makeFakeNut({ getWindows: jest.fn(async () => [other, target]) });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    const r = await c.focusWindow({ titleMatch: 'my app' });
    expect(r.focused).toBe(true);
    expect(target.focus).toHaveBeenCalled();
    expect(other.focus).not.toHaveBeenCalled();
  });

  test('focusWindow with regex', async () => {
    const target = fakeWin({ title: 'VS Code', bounds: {} });
    const nut = makeFakeNut({ getWindows: jest.fn(async () => [target]) });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    const r = await c.focusWindow({ titleMatch: /VS\s*Code/ });
    expect(r.focused).toBe(true);
  });

  test('focusWindow no match → focused:false', async () => {
    const nut = makeFakeNut({ getWindows: jest.fn(async () => []) });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    const r = await c.focusWindow({ titleMatch: 'nope' });
    expect(r.focused).toBe(false);
  });

  test('focusWindow without titleMatch → INVALID_INPUT', async () => {
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: makeFakeNut() });
    await expect(c.focusWindow({})).rejects.toMatchObject({ code: OSError.INVALID_INPUT });
  });

  test('windowFocus=false → UNSUPPORTED', async () => {
    const c = createOSController({ capabilities: WAYLAND_CAPS, nutImpl: makeFakeNut() });
    await expect(c.focusWindow({ titleMatch: 'x' })).rejects.toMatchObject({
      code: OSError.UNSUPPORTED,
    });
  });
});

// ─── error wrapping ────────────────────────────────────────────────────

describe('error wrapping', () => {
  test('"not authorized" raw error → PERMISSION_DENIED', async () => {
    const nut = makeFakeNut();
    nut.mouse.click = jest.fn(async () => { throw new Error('not authorized to send events'); });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await expect(c.mouseClick(1, 1)).rejects.toMatchObject({ code: OSError.PERMISSION_DENIED });
  });

  test('generic raw error → OP_FAILED', async () => {
    const nut = makeFakeNut();
    nut.mouse.click = jest.fn(async () => { throw new Error('some random failure'); });
    const c = createOSController({ capabilities: FULL_CAPS, nutImpl: nut });
    await expect(c.mouseClick(1, 1)).rejects.toMatchObject({ code: OSError.OP_FAILED });
  });

  test('UNSUPPORTED bubbles through unmodified', async () => {
    const c = createOSController({ capabilities: WAYLAND_CAPS, nutImpl: makeFakeNut() });
    await expect(c.mouseClick(1, 1)).rejects.toMatchObject({ code: OSError.UNSUPPORTED });
  });
});
