import assert from 'node:assert/strict';
import type { PathLike } from 'node:fs';
import test from 'node:test';
import {
  isCompiledMacOSHelperCurrent,
  MacOSWindowTracker,
  parseMacOSHelperOutput,
} from './macos-tracker';

test('parseMacOSHelperOutput parses minimized state', () => {
  assert.deepEqual(parseMacOSHelperOutput('minimized'), {
    geometry: null,
    focused: false,
    minimized: true,
  });
});

test('parseMacOSHelperOutput parses active focused state without geometry', () => {
  assert.deepEqual(parseMacOSHelperOutput('active'), {
    geometry: null,
    focused: true,
    active: true,
  });
});

test('parseMacOSHelperOutput parses inactive state without geometry', () => {
  assert.deepEqual(parseMacOSHelperOutput('inactive'), {
    geometry: null,
    focused: false,
    inactive: true,
  });
});

test('isCompiledMacOSHelperCurrent rejects binaries older than the Swift source', () => {
  const binaryPath = '/tmp/get-mpv-window-macos';
  const sourcePath = '/tmp/get-mpv-window-macos.swift';
  const statSync = (binaryMtimeMs: number, sourceMtimeMs: number) => (targetPath: PathLike) =>
    ({
      mtimeMs: String(targetPath) === binaryPath ? binaryMtimeMs : sourceMtimeMs,
    }) as never;
  const helperFs = {
    existsSync: () => true,
    statSync: statSync(1_000, 2_000),
  };

  assert.equal(isCompiledMacOSHelperCurrent(binaryPath, sourcePath, helperFs), false);

  helperFs.statSync = statSync(2_000, 1_000);

  assert.equal(isCompiledMacOSHelperCurrent(binaryPath, sourcePath, helperFs), true);
});

test('MacOSWindowTracker slows polling while focused target is stable', async () => {
  const scheduledDelays: number[] = [];
  let callIndex = 0;
  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper',
      helperType: 'binary',
    }),
    runHelper: async () => {
      callIndex += 1;
      return { stdout: '10,20,1280,720,1', stderr: '' };
    },
    fastPollIntervalMs: 250,
    stablePollIntervalMs: 1_000,
    setPollTimeout: ((_callback: () => void, delayMs: number) => {
      scheduledDelays.push(delayMs);
      return {} as ReturnType<typeof setTimeout>;
    }) as never,
    clearPollTimeout: (() => {}) as never,
  } as never);

  tracker.start();
  await new Promise((resolve) => setTimeout(resolve, 0));
  tracker.stop();

  assert.equal(callIndex, 1);
  assert.deepEqual(scheduledDelays, [1_000]);
});

test('MacOSWindowTracker keeps fast polling while target is not focused', async () => {
  const scheduledDelays: number[] = [];
  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper',
      helperType: 'binary',
    }),
    runHelper: async () => ({ stdout: '10,20,1280,720,0', stderr: '' }),
    fastPollIntervalMs: 250,
    stablePollIntervalMs: 1_000,
    setPollTimeout: ((_callback: () => void, delayMs: number) => {
      scheduledDelays.push(delayMs);
      return {} as ReturnType<typeof setTimeout>;
    }) as never,
    clearPollTimeout: (() => {}) as never,
  } as never);

  tracker.start();
  await new Promise((resolve) => setTimeout(resolve, 0));
  tracker.stop();

  assert.deepEqual(scheduledDelays, [250]);
});

test('MacOSWindowTracker keeps the last geometry through a single helper miss', async () => {
  let callIndex = 0;
  const outputs = [
    { stdout: '10,20,1280,720,1', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: '10,20,1280,720,1', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    trackingLossGraceMs: 0,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });
});

test('MacOSWindowTracker preserves target focus on helper not-found while retaining geometry', async () => {
  let callIndex = 0;
  const focusChanges: boolean[] = [];
  const outputs = [
    { stdout: '10,20,1280,720,1', stderr: '' },
    { stdout: 'not-found', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    trackingLossGraceMs: 1_500,
  });
  tracker.onWindowFocusChange = (focused) => {
    focusChanges.push(focused);
  };

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTargetWindowFocused(), true);

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  assert.equal(tracker.isTracking(), true);
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });
  assert.equal(tracker.isTargetWindowFocused(), true);
  assert.deepEqual(focusChanges, [true]);
});

test('MacOSWindowTracker keeps focused fullscreen target through active helper misses after grace', async () => {
  let callIndex = 0;
  let now = 1_000;
  const outputs = [
    { stdout: '10,20,1280,720,1', stderr: '' },
    { stdout: 'active', stderr: '' },
    { stdout: 'active', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    now: () => now,
    trackingLossGraceMs: 500,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowFocused(), true);

  now += 1_000;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  now += 1_000;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowFocused(), true);
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });
});

test('MacOSWindowTracker drops previously focused target after repeated not-found misses exceed grace', async () => {
  let callIndex = 0;
  let now = 1_000;
  const focusChanges: boolean[] = [];
  const outputs = [
    { stdout: '10,20,1280,720,1', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: 'not-found', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    now: () => now,
    trackingLossGraceMs: 500,
  });
  tracker.onWindowFocusChange = (focused) => {
    focusChanges.push(focused);
  };

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowFocused(), true);

  now += 1_000;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowFocused(), true);
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });
  assert.deepEqual(focusChanges, [true]);

  now += 1_000;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  assert.equal(tracker.isTracking(), false);
  assert.equal(tracker.isTargetWindowFocused(), false);
  assert.equal(tracker.getGeometry(), null);
  assert.deepEqual(focusChanges, [true, false]);
});

test('MacOSWindowTracker drops previously focused target after repeated helper execution failures exceed grace', async () => {
  let callIndex = 0;
  let now = 1_000;
  const focusChanges: boolean[] = [];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => {
      callIndex += 1;
      if (callIndex === 1) {
        return { stdout: '10,20,1280,720,1', stderr: '' };
      }
      throw Object.assign(new Error('helper timed out'), { stderr: 'timeout' });
    },
    now: () => now,
    trackingLossGraceMs: 500,
  });
  tracker.onWindowFocusChange = (focused) => {
    focusChanges.push(focused);
  };

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  now += 1_000;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  now += 1_000;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  assert.equal(tracker.isTracking(), false);
  assert.equal(tracker.isTargetWindowFocused(), false);
  assert.equal(tracker.getGeometry(), null);
  assert.deepEqual(focusChanges, [true, false]);
});

test('MacOSWindowTracker marks target unfocused on explicit inactive helper signal', async () => {
  let callIndex = 0;
  const focusChanges: boolean[] = [];
  const outputs = [
    { stdout: '10,20,1280,720,1', stderr: '' },
    { stdout: 'inactive', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    trackingLossGraceMs: 1_500,
  });
  tracker.onWindowFocusChange = (focused) => {
    focusChanges.push(focused);
  };

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));

  assert.equal(tracker.isTracking(), true);
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });
  assert.equal(tracker.isTargetWindowFocused(), false);
  assert.deepEqual(focusChanges, [true, false]);
});

test('MacOSWindowTracker refreshNow immediately samples frontmost mpv state', async () => {
  let callIndex = 0;
  const outputs = [
    { stdout: '10,20,1280,720,0', stderr: '' },
    { stdout: 'active', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
  });

  await (tracker as unknown as { refreshNow: () => Promise<void> }).refreshNow();
  assert.equal(tracker.isTargetWindowFocused(), false);

  await (tracker as unknown as { refreshNow: () => Promise<void> }).refreshNow();
  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowFocused(), true);
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });
});

test('MacOSWindowTracker drops tracking after consecutive helper misses', async () => {
  let callIndex = 0;
  const outputs = [
    { stdout: '10,20,1280,720,0', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: 'not-found', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    trackingLossGraceMs: 0,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowFocused(), false);

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), false);
  assert.equal(tracker.getGeometry(), null);
  assert.equal(tracker.isTargetWindowFocused(), false);
});

test('MacOSWindowTracker keeps tracking through repeated helper misses inside grace window', async () => {
  let callIndex = 0;
  let now = 1_000;
  const outputs = [
    { stdout: '10,20,1280,720,1', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: 'not-found', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    now: () => now,
    trackingLossGraceMs: 1_500,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);
  assert.deepEqual(tracker.getGeometry(), {
    x: 10,
    y: 20,
    width: 1280,
    height: 720,
  });
});

test('MacOSWindowTracker drops tracking after grace window expires', async () => {
  let callIndex = 0;
  let now = 1_000;
  const outputs = [
    { stdout: '10,20,1280,720,0', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: 'not-found', stderr: '' },
    { stdout: 'not-found', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    now: () => now,
    trackingLossGraceMs: 500,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowFocused(), false);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), false);
  assert.equal(tracker.getGeometry(), null);
});

test('MacOSWindowTracker reports minimized target when helper reports minimized', async () => {
  let callIndex = 0;
  let now = 1_000;
  const outputs = [
    { stdout: '10,20,1280,720,1', stderr: '' },
    { stdout: 'minimized', stderr: '' },
    { stdout: 'minimized', stderr: '' },
  ];

  const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
    resolveHelper: () => ({
      helperPath: 'helper.swift',
      helperType: 'swift',
    }),
    runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
    now: () => now,
    minimizedTrackingLossGraceMs: 200,
  });

  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTracking(), true);
  assert.equal(tracker.isTargetWindowMinimized(), false);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTargetWindowMinimized(), true);
  assert.equal(tracker.isTracking(), true);

  now += 250;
  (tracker as unknown as { pollGeometry: () => void }).pollGeometry();
  await new Promise((resolve) => setTimeout(resolve, 0));
  assert.equal(tracker.isTargetWindowMinimized(), true);
  assert.equal(tracker.isTracking(), false);
});
