import test from 'node:test';
import assert from 'node:assert/strict';
import {
  markJellyfinRemotePlaybackLoaded,
  createReportJellyfinRemoteProgressHandler,
  createReportJellyfinRemoteStoppedHandler,
  secondsToJellyfinTicks,
  shouldAutoLoadSecondarySubTrackForJellyfinPlayback,
} from './jellyfin-remote-playback';

test('secondsToJellyfinTicks converts seconds and clamps invalid values', () => {
  assert.equal(secondsToJellyfinTicks(1.25, 10_000_000), 12_500_000);
  assert.equal(secondsToJellyfinTicks(-3, 10_000_000), 0);
  assert.equal(secondsToJellyfinTicks(Number.NaN, 10_000_000), 0);
});

test('shouldAutoLoadSecondarySubTrackForJellyfinPlayback suppresses generic secondary autoload for active Jellyfin media', () => {
  assert.equal(shouldAutoLoadSecondarySubTrackForJellyfinPlayback(null, '/tmp/local.mkv'), true);
  assert.equal(
    shouldAutoLoadSecondarySubTrackForJellyfinPlayback(
      { itemId: 'item-1', playMethod: 'DirectPlay', loadedMediaPath: null },
      'http://pve-main:8096/Videos/item/stream',
    ),
    false,
  );
  assert.equal(
    shouldAutoLoadSecondarySubTrackForJellyfinPlayback(
      {
        itemId: 'item-1',
        playMethod: 'DirectPlay',
        loadedMediaPath: 'http://pve-main:8096/Videos/item/stream',
      },
      'http://pve-main:8096/Videos/item/stream',
    ),
    false,
  );
  assert.equal(
    shouldAutoLoadSecondarySubTrackForJellyfinPlayback(
      {
        itemId: 'item-1',
        playMethod: 'DirectPlay',
        loadedMediaPath: 'http://pve-main:8096/Videos/item/stream',
      },
      '/tmp/local.mkv',
    ),
    true,
  );
});

test('createReportJellyfinRemoteProgressHandler reports playback progress', async () => {
  let lastProgressAtMs = 0;
  const reportPayloads: Array<{ itemId: string; positionTicks: number; isPaused: boolean }> = [];

  const reportProgress = createReportJellyfinRemoteProgressHandler({
    getActivePlayback: () => ({
      itemId: 'item-1',
      mediaSourceId: undefined,
      playMethod: 'DirectPlay',
      audioStreamIndex: 1,
      subtitleStreamIndex: 2,
    }),
    clearActivePlayback: () => {},
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async (payload) => {
        reportPayloads.push({
          itemId: payload.itemId,
          positionTicks: payload.positionTicks,
          isPaused: payload.isPaused,
        });
      },
      reportStopped: async () => {},
    }),
    getMpvClient: () => ({
      requestProperty: async (name: string) => (name === 'time-pos' ? 2.5 : true),
    }),
    getNow: () => 5000,
    getLastProgressAtMs: () => lastProgressAtMs,
    setLastProgressAtMs: (value) => {
      lastProgressAtMs = value;
    },
    progressIntervalMs: 3000,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportProgress(true);

  assert.deepEqual(reportPayloads, [
    {
      itemId: 'item-1',
      positionTicks: 25_000_000,
      isPaused: true,
    },
  ]);
  assert.equal(lastProgressAtMs, 5000);
});

test('createReportJellyfinRemoteProgressHandler reports while remote websocket is disconnected', async () => {
  const reportPayloads: Array<{ positionTicks: number; isPaused: boolean }> = [];

  const reportProgress = createReportJellyfinRemoteProgressHandler({
    getActivePlayback: () => ({
      itemId: 'item-1',
      playMethod: 'DirectPlay',
    }),
    clearActivePlayback: () => {},
    getSession: () => ({
      isConnected: () => false,
      reportProgress: async (payload) => {
        reportPayloads.push({
          positionTicks: payload.positionTicks,
          isPaused: payload.isPaused,
        });
      },
      reportStopped: async () => {},
    }),
    getMpvClient: () => ({
      currentTimePos: 42,
      requestProperty: async (name: string) => (name === 'pause' ? false : 42),
    }),
    getNow: () => 5000,
    getLastProgressAtMs: () => 0,
    setLastProgressAtMs: () => {},
    progressIntervalMs: 3000,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportProgress(true);

  assert.deepEqual(reportPayloads, [{ positionTicks: 420_000_000, isPaused: false }]);
});

test('createReportJellyfinRemoteProgressHandler normalizes mpv pause strings', async () => {
  const reportPayloads: Array<{ isPaused: boolean }> = [];

  const reportProgress = createReportJellyfinRemoteProgressHandler({
    getActivePlayback: () => ({
      itemId: 'item-1',
      playMethod: 'DirectPlay',
    }),
    clearActivePlayback: () => {},
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async (payload) => {
        reportPayloads.push({ isPaused: payload.isPaused });
      },
      reportStopped: async () => {},
    }),
    getMpvClient: () => ({
      requestProperty: async (name: string) => (name === 'pause' ? 'yes' : 3),
    }),
    getNow: () => 5000,
    getLastProgressAtMs: () => 0,
    setLastProgressAtMs: () => {},
    progressIntervalMs: 3000,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportProgress(true);

  assert.deepEqual(reportPayloads, [{ isPaused: true }]);
});

test('createReportJellyfinRemoteProgressHandler respects debounce interval', async () => {
  let called = false;
  const reportProgress = createReportJellyfinRemoteProgressHandler({
    getActivePlayback: () => ({
      itemId: 'item-1',
      playMethod: 'DirectPlay',
    }),
    clearActivePlayback: () => {},
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async () => {
        called = true;
      },
      reportStopped: async () => {},
    }),
    getMpvClient: () => ({
      requestProperty: async () => 1,
    }),
    getNow: () => 4000,
    getLastProgressAtMs: () => 3500,
    setLastProgressAtMs: () => {},
    progressIntervalMs: 3000,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportProgress(false);
  assert.equal(called, false);
});

test('createReportJellyfinRemoteProgressHandler reports mpv seek jumps during debounce', async () => {
  let now = 5000;
  let lastProgressAtMs = 0;
  let position = 10;
  const reportPayloads: Array<{ positionTicks: number; eventName: string }> = [];

  const reportProgress = createReportJellyfinRemoteProgressHandler({
    getActivePlayback: () => ({
      itemId: 'item-1',
      playMethod: 'DirectPlay',
    }),
    clearActivePlayback: () => {},
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async (payload) => {
        reportPayloads.push({
          positionTicks: payload.positionTicks,
          eventName: payload.eventName,
        });
      },
      reportStopped: async () => {},
    }),
    getMpvClient: () => ({
      currentTimePos: position,
      requestProperty: async (name: string) => (name === 'pause' ? false : position),
    }),
    getNow: () => now,
    getLastProgressAtMs: () => lastProgressAtMs,
    setLastProgressAtMs: (value) => {
      lastProgressAtMs = value;
    },
    progressIntervalMs: 3000,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportProgress(true);
  now = 5500;
  position = 90;
  await reportProgress(false);

  assert.deepEqual(reportPayloads, [
    { positionTicks: 100_000_000, eventName: 'TimeUpdate' },
    { positionTicks: 900_000_000, eventName: 'TimeUpdate' },
  ]);
  assert.equal(lastProgressAtMs, 5500);
});

test('createReportJellyfinRemoteStoppedHandler reports stop and clears playback', async () => {
  let cleared = false;
  let stoppedPayload: {
    itemId: string;
    positionTicks?: number;
    failed?: boolean;
  } | null = null;
  const reportStopped = createReportJellyfinRemoteStoppedHandler({
    getActivePlayback: () => ({
      itemId: 'item-2',
      mediaSourceId: undefined,
      playMethod: 'Transcode',
      audioStreamIndex: null,
      subtitleStreamIndex: null,
    }),
    clearActivePlayback: () => {
      cleared = true;
    },
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async () => {},
      reportStopped: async (payload) => {
        stoppedPayload = {
          itemId: payload.itemId,
          positionTicks: payload.positionTicks,
          failed: payload.failed,
        };
      },
    }),
    getMpvClient: () => ({
      currentTimePos: 12.5,
      requestProperty: async () => {
        throw new Error('unloaded');
      },
    }),
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportStopped();
  assert.deepEqual(stoppedPayload, {
    itemId: 'item-2',
    positionTicks: 125_000_000,
    failed: false,
  });
  assert.equal(cleared, true);
});

test('createReportJellyfinRemoteStoppedHandler clears aborted playback that never loaded', async () => {
  let cleared = false;
  const reportStopped = createReportJellyfinRemoteStoppedHandler({
    getActivePlayback: () => ({
      itemId: 'item-2',
      mediaSourceId: undefined,
      playMethod: 'Transcode',
      audioStreamIndex: null,
      subtitleStreamIndex: null,
      loadedMediaPath: null,
    }),
    clearActivePlayback: () => {
      cleared = true;
    },
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async () => {},
      reportStopped: async () => {
        throw new Error('should not report stopped for unloaded media');
      },
    }),
    getMpvClient: () => null,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportStopped();

  assert.equal(cleared, true);
});

test('createReportJellyfinRemoteStoppedHandler reports stop while remote websocket is disconnected', async () => {
  let cleared = false;
  let stoppedPayload: {
    itemId: string;
    positionTicks?: number;
    failed?: boolean;
  } | null = null;
  const reportStopped = createReportJellyfinRemoteStoppedHandler({
    getActivePlayback: () => ({
      itemId: 'item-2',
      mediaSourceId: undefined,
      playMethod: 'Transcode',
      audioStreamIndex: null,
      subtitleStreamIndex: null,
      loadedMediaPath: 'https://stream.example/video.m3u8',
    }),
    clearActivePlayback: () => {
      cleared = true;
    },
    getSession: () => ({
      isConnected: () => false,
      reportProgress: async () => {},
      reportStopped: async (payload) => {
        stoppedPayload = {
          itemId: payload.itemId,
          positionTicks: payload.positionTicks,
          failed: payload.failed,
        };
      },
    }),
    getMpvClient: () => ({
      currentTimePos: 12.5,
    }),
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportStopped();

  assert.deepEqual(stoppedPayload, {
    itemId: 'item-2',
    positionTicks: 125_000_000,
    failed: false,
  });
  assert.equal(cleared, true);
});

test('createReportJellyfinRemoteStoppedHandler uses cached position after mpv unload reset', async () => {
  let cleared = false;
  const calls: Array<{ event: string; positionTicks?: number }> = [];
  const reportStopped = createReportJellyfinRemoteStoppedHandler({
    getActivePlayback: () =>
      ({
        itemId: 'item-2',
        mediaSourceId: undefined,
        playMethod: 'DirectPlay',
        audioStreamIndex: null,
        subtitleStreamIndex: null,
        loadedMediaPath: 'https://stream.example/video.m3u8',
        lastKnownPositionSeconds: 72.25,
      }) as never,
    clearActivePlayback: () => {
      cleared = true;
    },
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async (payload) => {
        calls.push({ event: 'progress', positionTicks: payload.positionTicks });
      },
      reportStopped: async (payload) => {
        calls.push({ event: 'stopped', positionTicks: payload.positionTicks });
      },
    }),
    getMpvClient: () => ({
      currentTimePos: 0,
    }),
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportStopped();

  assert.deepEqual(calls, [
    { event: 'progress', positionTicks: 722_500_000 },
    { event: 'stopped', positionTicks: 722_500_000 },
  ]);
  assert.equal(cleared, true);
});

test('createReportJellyfinRemoteStoppedHandler ignores unloaded active playback', async () => {
  let cleared = false;
  let stopped = false;
  const reportStopped = createReportJellyfinRemoteStoppedHandler({
    getActivePlayback: () =>
      ({
        itemId: 'item-2',
        playMethod: 'Transcode',
        loadedMediaPath: null,
      }) as never,
    clearActivePlayback: () => {
      cleared = true;
    },
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async () => {},
      reportStopped: async () => {
        stopped = true;
      },
    }),
    getMpvClient: () => ({
      currentTimePos: 0,
    }),
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportStopped();

  assert.equal(stopped, false);
  assert.equal(cleared, true);
});

test('createReportJellyfinRemoteProgressHandler caches last nonzero mpv position', async () => {
  let position = 42;
  let lastProgressAtMs = 0;
  const playback = {
    itemId: 'item-1',
    playMethod: 'DirectPlay' as const,
  };
  const reportProgress = createReportJellyfinRemoteProgressHandler({
    getActivePlayback: () => playback,
    clearActivePlayback: () => {},
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async () => {},
      reportStopped: async () => {},
    }),
    getMpvClient: () => ({
      currentTimePos: position,
      requestProperty: async (name: string) => (name === 'pause' ? false : position),
    }),
    getNow: () => 5000,
    getLastProgressAtMs: () => lastProgressAtMs,
    setLastProgressAtMs: (value) => {
      lastProgressAtMs = value;
    },
    progressIntervalMs: 3000,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportProgress(true);
  position = 0;
  await reportProgress(true);

  assert.equal((playback as { lastKnownPositionSeconds?: number }).lastKnownPositionSeconds, 42);
});

test('markJellyfinRemotePlaybackLoaded preserves the loaded marker on unload paths', () => {
  const playback = {
    itemId: 'item-2',
    playMethod: 'Transcode' as const,
    loadedMediaPath: 'https://stream.example/video.m3u8',
  };

  markJellyfinRemotePlaybackLoaded(playback, '');
  markJellyfinRemotePlaybackLoaded(playback, '   ');
  assert.equal(playback.loadedMediaPath, 'https://stream.example/video.m3u8');

  markJellyfinRemotePlaybackLoaded(playback, '  https://stream.example/next.m3u8  ');
  assert.equal(playback.loadedMediaPath, 'https://stream.example/next.m3u8');
});

test('createReportJellyfinRemoteStoppedHandler ignores startup stop churn before grace expires', async () => {
  let cleared = false;
  let stopped = false;
  const reportStopped = createReportJellyfinRemoteStoppedHandler({
    getActivePlayback: () =>
      ({
        itemId: 'item-2',
        playMethod: 'DirectPlay',
        loadedMediaPath: 'https://stream.example/video.m3u8',
        stopReportsAfterMs: 20_000,
      }) as never,
    clearActivePlayback: () => {
      cleared = true;
    },
    getSession: () => ({
      isConnected: () => true,
      reportProgress: async () => {},
      reportStopped: async () => {
        stopped = true;
      },
    }),
    getMpvClient: () => ({
      currentTimePos: 0,
    }),
    getNow: () => 12_000,
    ticksPerSecond: 10_000_000,
    logDebug: () => {},
  });

  await reportStopped();

  assert.equal(stopped, false);
  assert.equal(cleared, false);
});
