import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import type SegmentSinksStore from "../../segment_sinks";
import type { IBufferedChunk } from "../../segment_sinks";
import FreezeResolver from "../FreezeResolver";
import type { IFreezeResolverObservation } from "../FreezeResolver";

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/unbound-method */

const {
  mockGetCurrent,
  mockLogInfo,
  mockLogWarn,
  mockLogDebug,
  mockGetMonotonicTimeStamp,
} = vi.hoisted(() => ({
  mockGetCurrent: vi.fn(() => ({
    UNFREEZING_SEEK_DELAY: 3000,
    UNFREEZING_DELTA_POSITION: 0.001,
    FREEZING_FLUSH_FAILURE_DELAY: {
      MINIMUM: 1000,
      MAXIMUM: 10000,
      POSITION_DELTA: 0.1,
    },
  })),
  mockLogInfo: vi.fn(),
  mockLogWarn: vi.fn(),
  mockLogDebug: vi.fn(),
  mockGetMonotonicTimeStamp: vi.fn(),
}));

vi.mock("../../../config", () => ({
  default: {
    getCurrent: mockGetCurrent,
  },
}));

vi.mock("../../../log", () => ({
  default: {
    info: mockLogInfo,
    warn: mockLogWarn,
    debug: mockLogDebug,
  },
}));

vi.mock("../../../utils/monotonic_timestamp", () => ({
  default: mockGetMonotonicTimeStamp,
}));

describe("FreezeResolver", () => {
  let freezeResolver: FreezeResolver;
  let mockSegmentSinksStore: SegmentSinksStore;
  let mockTimestamp: number;

  const createMockObservation = (
    overrides: Partial<IFreezeResolverObservation> = {},
  ): IFreezeResolverObservation => ({
    readyState: 4,
    rebuffering: null,
    freezing: null,
    bufferGap: 10,
    position: {
      getPolled: () => 100,
    } as any,
    fullyLoaded: false,
    ...overrides,
  });

  const createMockSegmentSinksStore = (): SegmentSinksStore =>
    ({
      getStatus: vi.fn(() => ({
        type: "initialized",
        value: {
          getLastKnownInventory: vi.fn(() => []),
        },
      })),
    }) as any;

  beforeEach(() => {
    mockTimestamp = 10000;
    mockGetMonotonicTimeStamp.mockImplementation(() => mockTimestamp);
    mockSegmentSinksStore = createMockSegmentSinksStore();
    freezeResolver = new FreezeResolver(mockSegmentSinksStore);
  });

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

  describe("onNewObservation - No freeze scenarios", () => {
    it("should return null when playback is not frozen", () => {
      const observation = createMockObservation({
        readyState: 4,
        freezing: null,
        rebuffering: null,
      });
      const result = freezeResolver.onNewObservation(observation);
      expect(result).toBeNull();
    });

    it("should return null when readyState is 1 but buffer gap is too small", () => {
      const observation = createMockObservation({
        readyState: 1,
        bufferGap: 3, // Less than MINIMUM_BUFFER_GAP_AT_READY_STATE_1_BEFORE_FREEZING (6)
        freezing: null,
        rebuffering: null,
        fullyLoaded: false,
      });
      const result = freezeResolver.onNewObservation(observation);
      expect(result).toBeNull();
    });

    it("should ignore freeze when within ignore period", () => {
      const observation1 = createMockObservation({
        freezing: { timestamp: mockTimestamp },
      });

      // First observation triggers a flush
      mockTimestamp = 10000;
      freezeResolver.onNewObservation(observation1);
      mockTimestamp = 13100; // Within UNFREEZING_SEEK_DELAY

      const result = freezeResolver.onNewObservation(observation1);
      expect(result).not.toBeNull();

      // Second observation within ignore period
      mockTimestamp = 13200; // Still within MINIMUM_TIME_BETWEEN_FREEZE_HANDLING (6000ms from flush)
      const observation2 = createMockObservation({
        freezing: { timestamp: 13200 },
      });

      const result2 = freezeResolver.onNewObservation(observation2);
      expect(result2).toBeNull();
    });
  });

  describe("onNewObservation - Freeze detection", () => {
    it("should detect freeze when freezing status is set", () => {
      const observation = createMockObservation({
        freezing: { timestamp: mockTimestamp - 4000 },
        bufferGap: 10,
      });

      const result = freezeResolver.onNewObservation(observation);

      expect(result).not.toBeNull();
      expect(mockLogInfo).toHaveBeenCalledWith(
        "Freeze",
        "Freeze detected",
        expect.any(Object),
      );
    });

    it("should detect freeze when readyState is 1 with sufficient buffer", () => {
      const observation = createMockObservation({
        readyState: 1,
        bufferGap: 8, // Greater than MINIMUM_BUFFER_GAP_AT_READY_STATE_1_BEFORE_FREEZING
        rebuffering: {
          timestamp: mockTimestamp - 4000,
          reason: "not-ready",
          position: undefined,
        },
      });

      const result = freezeResolver.onNewObservation(observation);
      expect(result).not.toBeNull();
    });

    it("should detect freeze when fully loaded with readyState 1", () => {
      const observation = createMockObservation({
        readyState: 1,
        bufferGap: 2,
        fullyLoaded: true,
        rebuffering: {
          timestamp: mockTimestamp - 4000,
          reason: "not-ready",
          position: undefined,
        },
      });

      const result = freezeResolver.onNewObservation(observation);
      expect(result).not.toBeNull();
    });
  });

  describe("onNewObservation - Flush strategy", () => {
    it("should return flush resolution after freeze delay", () => {
      const freezeTimestamp = mockTimestamp - 4000;
      const observation = createMockObservation({
        freezing: { timestamp: freezeTimestamp },
        position: { getPolled: () => 100 } as any,
      });

      const result = freezeResolver.onNewObservation(observation);

      expect(result).toEqual({
        type: "flush",
        value: { relativeSeek: 0.001 },
      });
      expect(mockLogDebug).toHaveBeenCalledWith("Freeze", "Trying to flush to un-freeze");
    });

    it("should not flush before UNFREEZING_SEEK_DELAY has passed", () => {
      const freezeTimestamp = mockTimestamp - 2000; // Less than UNFREEZING_SEEK_DELAY (3000)
      const observation = createMockObservation({
        freezing: { timestamp: freezeTimestamp },
      });

      const result = freezeResolver.onNewObservation(observation);

      expect(result).toBeNull();
    });
  });

  describe("onNewObservation - Decipherability checks", () => {
    it("should reload when buffer has undecipherable data", () => {
      const mockSegment: IBufferedChunk = {
        infos: {
          representation: {
            decipherable: false,
            uniqueId: "rep1",
          } as any,
        },
        start: 90,
        end: 110,
      } as any;

      mockSegmentSinksStore.getStatus = vi.fn(
        () =>
          ({
            type: "initialized",
            value: {
              getLastKnownInventory: vi.fn(() => [mockSegment]),
            },
          }) as any,
      );

      const observation = createMockObservation({
        rebuffering: {
          timestamp: mockTimestamp - 5000,
          reason: "not-ready",
          position: undefined,
        },
        readyState: 1,
        bufferGap: 8,
      });

      const result = freezeResolver.onNewObservation(observation);

      expect(result).toEqual({
        type: "reload",
        value: null,
      });
      expect(mockLogWarn).toHaveBeenCalledWith(
        "Freeze",
        expect.stringContaining("undecipherable segments"),
      );
    });

    it("should first flush when frozen with only decipherable encrypted data", () => {
      const mockSegment: IBufferedChunk = {
        infos: {
          representation: {
            decipherable: true,
            uniqueId: "rep1",
          } as any,
        },
        start: 90,
        end: 110,
      } as any;

      mockSegmentSinksStore.getStatus = vi.fn(
        () =>
          ({
            type: "initialized",
            value: {
              getLastKnownInventory: vi.fn(() => [mockSegment]),
            },
          }) as any,
      );

      const observation = createMockObservation({
        rebuffering: {
          timestamp: mockTimestamp - 5000,
          reason: "not-ready",
          position: undefined,
        },
        readyState: 1,
        bufferGap: 8,
      });

      const result = freezeResolver.onNewObservation(observation);
      expect(result).toEqual({
        type: "flush",
        value: {
          relativeSeek: 0.001,
        },
      });
    });
  });

  describe("onNewObservation - Failed flush handling", () => {
    it("should reload when flush fails and no representation change detected", () => {
      // First: trigger a flush
      mockTimestamp = 10000;
      const observation1 = createMockObservation({
        freezing: { timestamp: 6000 },
        position: { getPolled: () => 100 } as any,
      });

      const flushResult = freezeResolver.onNewObservation(observation1);
      expect(flushResult?.type).toBe("flush");

      // Second: still frozen after flush, need to wait beyond the ignore period
      // MINIMUM_TIME_BETWEEN_FREEZE_HANDLING is 6000ms
      mockTimestamp = 17500; // 7500ms after flush, beyond ignore period (16000 + 1500)
      const observation2 = createMockObservation({
        freezing: { timestamp: 17500 },
        position: { getPolled: () => 100.001 } as any, // Very close to flush position
      });

      const result = freezeResolver.onNewObservation(observation2);

      expect(result).toEqual({
        type: "reload",
        value: null,
      });
    });

    it("should avoid representation when freeze happens on representation switch", () => {
      const mockSegment1: IBufferedChunk = {
        infos: {
          representation: { uniqueId: "rep1", bitrate: 1000000 } as any,
          adaptation: { id: "video-ada" } as any,
          period: { id: "period1" } as any,
        },
        start: 90,
        end: 100,
        bufferedStart: 90,
        bufferedEnd: 100,
      } as any;

      const mockSegment2: IBufferedChunk = {
        infos: {
          representation: { uniqueId: "rep2", bitrate: 2000000 } as any,
          adaptation: { id: "video-ada" } as any,
          period: { id: "period1" } as any,
        },
        start: 100,
        end: 110,
        bufferedStart: 100,
        bufferedEnd: 110,
      } as any;

      // Build history with representation switch
      mockTimestamp = 10000;
      mockSegmentSinksStore.getStatus = vi.fn(
        () =>
          ({
            type: "initialized",
            value: {
              getLastKnownInventory: vi.fn(() => [mockSegment1]),
            },
          }) as any,
      );
      freezeResolver.onNewObservation(
        createMockObservation({ position: { getPolled: () => 95 } as any }),
      );

      mockTimestamp = 11000;
      mockSegmentSinksStore.getStatus = vi.fn(
        () =>
          ({
            type: "initialized",
            value: {
              getLastKnownInventory: vi.fn(() => [mockSegment2]),
            },
          }) as any,
      );
      freezeResolver.onNewObservation(
        createMockObservation({ position: { getPolled: () => 105 } as any }),
      );

      // Trigger flush
      mockTimestamp = 15000;
      const observation1 = createMockObservation({
        freezing: { timestamp: 11000 }, // Frozen for 4000ms
        position: { getPolled: () => 105 } as any,
      });
      const flushResult = freezeResolver.onNewObservation(observation1);
      expect(flushResult?.type).toBe("flush");

      // Still frozen after flush, need to wait beyond ignore period (6000ms from flush at 15000 = 21000)
      mockTimestamp = 22000;
      const observation2 = createMockObservation({
        freezing: { timestamp: 22000 },
        position: { getPolled: () => 105.001 } as any,
      });

      const result = freezeResolver.onNewObservation(observation2);

      expect(result).toEqual({
        type: "avoid-representations",
        value: expect.arrayContaining([
          expect.objectContaining({
            representation: expect.objectContaining({ uniqueId: "rep2" }),
          }),
        ]),
      });
    });

    it("should reload when freeze happens on period switch", () => {
      const mockSegment1: IBufferedChunk = {
        infos: {
          representation: { uniqueId: "rep1" } as any,
          adaptation: { id: "video-ada" } as any,
          period: { id: "period1" } as any,
        },
        start: 90,
        end: 100,
        bufferedStart: 90,
        bufferedEnd: 100,
      } as any;

      const mockSegment2: IBufferedChunk = {
        infos: {
          representation: { uniqueId: "rep2" } as any,
          adaptation: { id: "video-ada" } as any,
          period: { id: "period2" } as any, // Different period
        },
        start: 100,
        end: 110,
        bufferedStart: 100,
        bufferedEnd: 110,
      } as any;

      // Build history with period switch
      mockTimestamp = 10000;
      mockSegmentSinksStore.getStatus = vi.fn(
        () =>
          ({
            type: "initialized",
            value: {
              getLastKnownInventory: vi.fn(() => [mockSegment1]),
            },
          }) as any,
      );
      freezeResolver.onNewObservation(
        createMockObservation({ position: { getPolled: () => 95 } as any }),
      );

      mockTimestamp = 11000;
      mockSegmentSinksStore.getStatus = vi.fn(
        () =>
          ({
            type: "initialized",
            value: {
              getLastKnownInventory: vi.fn(() => [mockSegment2]),
            },
          }) as any,
      );
      freezeResolver.onNewObservation(
        createMockObservation({ position: { getPolled: () => 105 } as any }),
      );

      // Trigger flush
      mockTimestamp = 15000;
      const flushResult = freezeResolver.onNewObservation(
        createMockObservation({
          freezing: { timestamp: 11000 }, // Frozen for 4000ms
          position: { getPolled: () => 105 } as any,
        }),
      );
      expect(flushResult?.type).toBe("flush");

      // Still frozen after flush, need to wait beyond ignore period (6000ms from flush at 15000 = 21000)
      mockTimestamp = 22000;
      const result = freezeResolver.onNewObservation(
        createMockObservation({
          freezing: { timestamp: 22000 },
          position: { getPolled: () => 105.001 } as any,
        }),
      );

      expect(result).toEqual({
        type: "reload",
        value: null,
      });
    });
  });

  describe("History management", () => {
    it("should maintain segment history for audio and video", () => {
      const mockSegment: IBufferedChunk = {
        infos: {
          representation: { uniqueId: "rep1" } as any,
        },
        start: 90,
        bufferedStart: 90,
        end: 110,
        bufferedEnd: 110,
      } as any;

      mockSegmentSinksStore.getStatus = vi.fn(
        () =>
          ({
            type: "initialized",
            value: {
              getLastKnownInventory: vi.fn(() => [mockSegment]),
            },
          }) as any,
      );

      const observation = createMockObservation({
        position: { getPolled: () => 100 } as any,
      });

      freezeResolver.onNewObservation(observation);

      // Verify history is tracked (implicitly tested through representation avoidance)
      expect(mockSegmentSinksStore.getStatus).toHaveBeenCalledWith("audio");
      expect(mockSegmentSinksStore.getStatus).toHaveBeenCalledWith("video");
    });

    it("should limit history to 100 entries per track type", () => {
      // Create many observations to exceed history limit
      for (let i = 0; i < 150; i++) {
        mockTimestamp += 100;
        freezeResolver.onNewObservation(
          createMockObservation({
            position: { getPolled: () => i } as any,
          }),
        );
      }

      // The test passes if no errors are thrown
      // History should be limited internally
      expect(true).toBe(true);
    });
  });
});
