import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// Mock video element for testing seek behavior
function createMockVideoElement(options: {
  paused?: boolean;
  autoplay?: boolean;
  currentTime?: number;
} = {}) {
  const listeners: Record<string, (() => void)[]> = {};

  return {
    paused: options.paused ?? true,
    autoplay: options.autoplay ?? true,
    currentTime: options.currentTime ?? 0,

    pause: vi.fn(function(this: any) {
      this.paused = true;
    }),

    play: vi.fn(function(this: any) {
      this.paused = false;
      return Promise.resolve();
    }),

    addEventListener: vi.fn((event: string, handler: () => void) => {
      if (!listeners[event]) listeners[event] = [];
      listeners[event].push(handler);
    }),

    removeEventListener: vi.fn((event: string, handler: () => void) => {
      if (listeners[event]) {
        listeners[event] = listeners[event].filter(h => h !== handler);
      }
    }),

    // Helper to trigger events in tests
    _triggerEvent: (event: string) => {
      listeners[event]?.forEach(h => h());
    },

    _getListeners: () => listeners,
  };
}

describe("VideoPlayer Resume Logic", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe("handleCanPlay seek behavior", () => {
    it("should pause video before seeking to prevent autoplay from starting at position 0", async () => {
      const videoElement = createMockVideoElement({ paused: false, autoplay: true });

      // Simulate the handleCanPlay logic
      const initialPosition = 60;
      const hasPerformedInitialSeek = false;

      if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
        const wasPlaying = !videoElement.paused;
        videoElement.pause();

        expect(videoElement.pause).toHaveBeenCalled();
        expect(wasPlaying).toBe(true);
      }
    });

    it("should set currentTime to initial position", async () => {
      const videoElement = createMockVideoElement();
      const initialPosition = 120;

      videoElement.currentTime = initialPosition;

      expect(videoElement.currentTime).toBe(120);
    });

    it("should wait for seeked event before resuming playback", async () => {
      const videoElement = createMockVideoElement({ paused: false, autoplay: true });
      const initialPosition = 60;

      // Simulate handleCanPlay logic
      videoElement.pause();
      videoElement.currentTime = initialPosition;

      // Create the promise that waits for seeked
      const seekPromise = new Promise<void>((resolve) => {
        const onSeeked = () => {
          videoElement.removeEventListener("seeked", onSeeked);
          resolve();
        };
        videoElement.addEventListener("seeked", onSeeked);
      });

      // Verify listener was added
      expect(videoElement.addEventListener).toHaveBeenCalledWith("seeked", expect.any(Function));

      // Simulate seek completion
      videoElement._triggerEvent("seeked");

      await seekPromise;

      // Verify listener was removed after seek
      expect(videoElement.removeEventListener).toHaveBeenCalledWith("seeked", expect.any(Function));
    });

    it("should resume playback after seek completes when autoplay is enabled", async () => {
      const videoElement = createMockVideoElement({ paused: false, autoplay: true });
      const initialPosition = 60;

      // Simulate handleCanPlay logic
      const wasPlaying = !videoElement.paused;
      videoElement.pause();
      videoElement.currentTime = initialPosition;

      // Wait for seeked
      const seekPromise = new Promise<void>((resolve) => {
        const onSeeked = () => {
          videoElement.removeEventListener("seeked", onSeeked);
          resolve();
        };
        videoElement.addEventListener("seeked", onSeeked);
      });

      videoElement._triggerEvent("seeked");
      await seekPromise;

      // Resume playback
      if (wasPlaying || videoElement.autoplay) {
        await videoElement.play();
      }

      expect(videoElement.play).toHaveBeenCalled();
    });

    it("should not resume playback if video was paused and has no autoplay", async () => {
      const videoElement = createMockVideoElement({ paused: true, autoplay: false });
      const initialPosition = 60;

      const wasPlaying = !videoElement.paused;
      videoElement.pause();
      videoElement.currentTime = initialPosition;

      // Resume playback check
      if (wasPlaying || videoElement.autoplay) {
        await videoElement.play();
      }

      expect(videoElement.play).not.toHaveBeenCalled();
    });

    it("should have fallback timeout in case seeked event doesn't fire", async () => {
      const videoElement = createMockVideoElement();
      const initialPosition = 60;

      videoElement.currentTime = initialPosition;

      let resolved = false;
      const seekPromise = new Promise<void>((resolve) => {
        const onSeeked = () => {
          videoElement.removeEventListener("seeked", onSeeked);
          resolve();
        };
        videoElement.addEventListener("seeked", onSeeked);

        // Fallback timeout
        setTimeout(() => {
          videoElement.removeEventListener("seeked", onSeeked);
          resolved = true;
          resolve();
        }, 2000);
      });

      // Don't trigger seeked event - rely on timeout
      vi.advanceTimersByTime(2000);

      await seekPromise;

      expect(resolved).toBe(true);
    });

    it("should not seek if initialPosition is 0", () => {
      const videoElement = createMockVideoElement();
      const initialPosition = 0;
      const hasPerformedInitialSeek = false;

      let seekPerformed = false;
      if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
        seekPerformed = true;
      }

      expect(seekPerformed).toBe(false);
    });

    it("should not seek if hasPerformedInitialSeek is true", () => {
      const videoElement = createMockVideoElement();
      const initialPosition = 60;
      const hasPerformedInitialSeek = true;

      let seekPerformed = false;
      if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
        seekPerformed = true;
      }

      expect(seekPerformed).toBe(false);
    });

    it("should not seek if videoElement is null", () => {
      const videoElement = null;
      const initialPosition = 60;
      const hasPerformedInitialSeek = false;

      let seekPerformed = false;
      if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
        seekPerformed = true;
      }

      expect(seekPerformed).toBe(false);
    });
  });

  describe("hasPerformedInitialSeek flag", () => {
    it("should be set to true after seek is initiated", () => {
      const videoElement = createMockVideoElement();
      const initialPosition = 60;
      let hasPerformedInitialSeek = false;

      if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
        hasPerformedInitialSeek = true;
        videoElement.currentTime = initialPosition;
      }

      expect(hasPerformedInitialSeek).toBe(true);
    });

    it("should be reset to false when streamUrl changes", () => {
      let hasPerformedInitialSeek = true;
      let currentStreamUrl = "url1";

      // Simulate $effect when streamUrl changes
      const newStreamUrl = "url2";
      if (newStreamUrl !== currentStreamUrl) {
        currentStreamUrl = newStreamUrl;
        hasPerformedInitialSeek = false;
      }

      expect(hasPerformedInitialSeek).toBe(false);
    });

    it("should prevent duplicate seeks on multiple canplay events", () => {
      const videoElement = createMockVideoElement();
      const initialPosition = 60;
      let hasPerformedInitialSeek = false;
      let seekCount = 0;

      // First canplay
      if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
        hasPerformedInitialSeek = true;
        videoElement.currentTime = initialPosition;
        seekCount++;
      }

      // Second canplay (shouldn't seek)
      if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
        hasPerformedInitialSeek = true;
        videoElement.currentTime = initialPosition;
        seekCount++;
      }

      expect(seekCount).toBe(1);
    });
  });

  describe("initialPosition change handling", () => {
    it("should seek when initialPosition changes after initial seek was done", () => {
      const videoElement = createMockVideoElement();
      let hasPerformedInitialSeek = true;
      const isMediaReady = true;
      let currentTime = 60;

      // Simulate new position
      const newPosition = 120;

      if (newPosition && newPosition > 0 && isMediaReady && videoElement && hasPerformedInitialSeek) {
        hasPerformedInitialSeek = false;
        videoElement.currentTime = newPosition;
        currentTime = newPosition;
      }

      expect(videoElement.currentTime).toBe(120);
      expect(currentTime).toBe(120);
      expect(hasPerformedInitialSeek).toBe(false);
    });

    it("should not seek if media is not ready", () => {
      const videoElement = createMockVideoElement();
      const hasPerformedInitialSeek = true;
      const isMediaReady = false;
      const newPosition = 120;

      let seekTriggered = false;
      if (newPosition && newPosition > 0 && isMediaReady && videoElement && hasPerformedInitialSeek) {
        seekTriggered = true;
      }

      expect(seekTriggered).toBe(false);
    });

    it("should not seek if initial seek hasn't been performed yet", () => {
      const videoElement = createMockVideoElement();
      const hasPerformedInitialSeek = false;
      const isMediaReady = true;
      const newPosition = 120;

      let seekTriggered = false;
      if (newPosition && newPosition > 0 && isMediaReady && videoElement && hasPerformedInitialSeek) {
        seekTriggered = true;
      }

      expect(seekTriggered).toBe(false);
    });
  });

  describe("seekOffset handling for transcoded streams", () => {
    it("should reset seekOffset to 0 when streamUrl changes", () => {
      let seekOffset = 120;
      let currentStreamUrl = "url1";

      // Simulate $effect when streamUrl changes
      const newStreamUrl = "url2";
      if (newStreamUrl !== currentStreamUrl) {
        currentStreamUrl = newStreamUrl;
        seekOffset = 0;
      }

      expect(seekOffset).toBe(0);
    });

    it("should add seekOffset to currentTime for transcoded streams", () => {
      const seekOffset = 60;
      const videoElementTime = 30; // Video thinks it's at 30s

      const currentTime = seekOffset + videoElementTime;

      expect(currentTime).toBe(90); // Actual position is 90s
    });
  });

  describe("error handling", () => {
    it("should handle seek errors gracefully", async () => {
      const videoElement = createMockVideoElement();
      const initialPosition = 60;
      let errorCaught = false;

      // Simulate a video element that throws on currentTime set
      Object.defineProperty(videoElement, 'currentTime', {
        set: () => { throw new Error('Seek not allowed'); },
        get: () => 0,
      });

      try {
        videoElement.currentTime = initialPosition;
      } catch (err) {
        errorCaught = true;
      }

      expect(errorCaught).toBe(true);
    });

    it("should handle play() rejection gracefully", async () => {
      const videoElement = createMockVideoElement();
      videoElement.play = vi.fn().mockRejectedValue(new Error('Autoplay blocked'));

      let errorCaught = false;
      try {
        await videoElement.play();
      } catch (err) {
        errorCaught = true;
      }

      expect(errorCaught).toBe(true);
    });
  });
});

describe("Resume Dialog Logic", () => {
  describe("progress eligibility", () => {
    it("should show resume dialog when watched > 30 seconds and < 90% complete", () => {
      const positionSeconds = 60;
      const totalSeconds = 3600; // 1 hour video
      const progressPercent = (positionSeconds / totalSeconds) * 100;

      const shouldShow = positionSeconds > 30 && progressPercent < 90;

      expect(shouldShow).toBe(true);
    });

    it("should not show resume dialog when watched <= 30 seconds", () => {
      const positionSeconds = 25;
      const totalSeconds = 3600;
      const progressPercent = (positionSeconds / totalSeconds) * 100;

      const shouldShow = positionSeconds > 30 && progressPercent < 90;

      expect(shouldShow).toBe(false);
    });

    it("should not show resume dialog when >= 90% complete", () => {
      const positionSeconds = 3300; // 55 minutes of 1 hour video
      const totalSeconds = 3600;
      const progressPercent = (positionSeconds / totalSeconds) * 100;

      const shouldShow = positionSeconds > 30 && progressPercent < 90;

      expect(shouldShow).toBe(false);
    });

    it("should handle edge case at exactly 30 seconds", () => {
      const positionSeconds = 30;
      const totalSeconds = 3600;
      const progressPercent = (positionSeconds / totalSeconds) * 100;

      const shouldShow = positionSeconds > 30 && progressPercent < 90;

      expect(shouldShow).toBe(false); // > 30, not >= 30
    });

    it("should handle edge case at exactly 90%", () => {
      const positionSeconds = 3240; // Exactly 90% of 3600
      const totalSeconds = 3600;
      const progressPercent = (positionSeconds / totalSeconds) * 100;

      const shouldShow = positionSeconds > 30 && progressPercent < 90;

      expect(shouldShow).toBe(false); // < 90, not <= 90
    });
  });

  describe("position tick conversion", () => {
    it("should convert ticks to seconds correctly", () => {
      const positionTicks = 600_000_000; // 60 seconds in ticks
      const positionSeconds = positionTicks / 10_000_000;

      expect(positionSeconds).toBe(60);
    });

    it("should convert seconds to ticks correctly", () => {
      const positionSeconds = 120;
      const positionTicks = positionSeconds * 10_000_000;

      expect(positionTicks).toBe(1_200_000_000);
    });

    it("should handle large tick values", () => {
      const positionTicks = 36_000_000_000; // 1 hour in ticks
      const positionSeconds = positionTicks / 10_000_000;

      expect(positionSeconds).toBe(3600);
    });
  });
});
