import { TaskStatus } from "@lit/task";
import { customElement } from "lit/decorators.js";
import { afterEach, beforeEach, describe, vi } from "vitest";
import { test as baseTest } from "../../../../test/useMSW.js";
import type { VideoRendition } from "../../../transcoding/types";
import { EFVideo } from "../../EFVideo";
import {
  computeBufferQueue,
  computeSegmentRange,
  handleSeekTimeChange,
  type MediaBufferDependencies,
  manageMediaBuffer,
} from "../shared/BufferUtils";
import {
  makeVideoBufferTask,
  type VideoBufferConfig,
  type VideoBufferState,
} from "./makeVideoBufferTask";

@customElement("test-media-video-buffer")
class TestMediaVideoBuffer extends EFVideo {}

declare global {
  interface HTMLElementTagNameMap {
    "test-media-video-buffer": TestMediaVideoBuffer;
  }
}

const test = baseTest.extend<{
  element: TestMediaVideoBuffer;
  mockVideoRendition: VideoRendition;
  mockConfig: VideoBufferConfig;
  mockState: VideoBufferState;
  mockDeps: MediaBufferDependencies<VideoRendition>;
  mockSignal: AbortSignal;
}>({
  element: async ({}, use) => {
    const element = document.createElement("test-media-video-buffer");
    await use(element);
    element.remove();
  },

  mockVideoRendition: async ({}, use) => {
    const rendition = {
      trackId: 1,
      src: "test-video.mp4",
      segmentDurationMs: 1000, // 1 second per segment
    };
    await use(rendition);
  },

  mockConfig: async ({}, use) => {
    const config = {
      bufferDurationMs: 5000, // 5 seconds
      maxParallelFetches: 2,
      enableBuffering: true,
      enableContinuousBuffering: false, // Disable for predictable testing
    };
    await use(config);
  },

  mockState: async ({}, use) => {
    const state = {
      currentSeekTimeMs: 0,
      activeRequests: new Set<number>(),
      cachedSegments: new Set<number>(),
      requestQueue: [],
    };
    await use(state);
  },

  mockDeps: async ({ mockVideoRendition }, use) => {
    const deps = {
      computeSegmentId: vi.fn(
        async (timeMs: number, rendition: VideoRendition) => {
          return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
        },
      ),
      fetchSegment: vi.fn().mockResolvedValue(new ArrayBuffer(1024)),
      getRendition: vi.fn().mockResolvedValue(mockVideoRendition),
      logError: vi.fn(),
    };
    await use(deps);
  },

  mockSignal: async ({}, use) => {
    const mockSignal = new AbortController().signal;
    await use(mockSignal);
  },
});

describe("computeSegmentRange", () => {
  test("computes segment range for time range", ({
    mockVideoRendition,
    expect,
  }) => {
    const syncComputeSegmentId = vi.fn(
      (timeMs: number, rendition: VideoRendition) => {
        return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
      },
    );

    const startTimeMs = 2000; // 2 seconds
    const endTimeMs = 6000; // 6 seconds

    const segments = computeSegmentRange(
      startTimeMs,
      endTimeMs,
      mockVideoRendition,
      syncComputeSegmentId,
    );

    expect(segments).toEqual([2, 3, 4, 5, 6]);
    expect(syncComputeSegmentId).toHaveBeenCalledWith(
      startTimeMs,
      mockVideoRendition,
    );
    expect(syncComputeSegmentId).toHaveBeenCalledWith(
      endTimeMs,
      mockVideoRendition,
    );
  });

  test("handles zero start time", ({ mockVideoRendition, expect }) => {
    const syncComputeSegmentId = (
      timeMs: number,
      rendition: VideoRendition,
    ) => {
      return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
    };

    const segments = computeSegmentRange(
      0,
      3000,
      mockVideoRendition,
      syncComputeSegmentId,
    );
    expect(segments).toEqual([0, 1, 2, 3]);
  });

  test("handles single segment range", ({ mockVideoRendition, expect }) => {
    const syncComputeSegmentId = (
      timeMs: number,
      rendition: VideoRendition,
    ) => {
      return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
    };

    const segments = computeSegmentRange(
      2500,
      2800,
      mockVideoRendition,
      syncComputeSegmentId,
    );
    expect(segments).toEqual([2]);
  });

  test("uses default segment duration when missing", ({ expect }) => {
    const syncComputeSegmentId = (
      timeMs: number,
      rendition: VideoRendition,
    ) => {
      return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
    };
    const renditionWithoutDuration = { trackId: 1, src: "test" };

    const segments = computeSegmentRange(
      2000,
      5000,
      renditionWithoutDuration as any,
      syncComputeSegmentId,
    );

    // Should use default 1000ms duration and call syncComputeSegmentId for segments 2, 3, 4, 5
    // Since syncComputeSegmentId always returns Math.floor(timeMs / 1000), result is [2, 3, 4, 5]
    expect(segments).toEqual([2, 3, 4, 5]);
  });

  test("returns empty array when computeSegmentId returns undefined", ({
    mockVideoRendition,
    expect,
  }) => {
    const computeSegmentId = vi.fn().mockReturnValue(undefined);

    const segments = computeSegmentRange(
      0,
      1000,
      mockVideoRendition,
      computeSegmentId,
    );
    expect(segments).toEqual([]);
  });
});

describe("computeBufferQueue", () => {
  test("filters out active requests and cached segments", ({ expect }) => {
    const desiredSegments = [1, 2, 3, 4, 5];
    const activeRequests = new Set([2, 4]);
    const cachedSegments = new Set([1]);

    const queue = computeBufferQueue(
      desiredSegments,
      activeRequests,
      cachedSegments,
    );
    expect(queue).toEqual([3, 5]);
  });

  test("returns empty queue when all segments are active or cached", ({
    expect,
  }) => {
    const desiredSegments = [1, 2, 3];
    const activeRequests = new Set([1, 2]);
    const cachedSegments = new Set([3]);

    const queue = computeBufferQueue(
      desiredSegments,
      activeRequests,
      cachedSegments,
    );
    expect(queue).toEqual([]);
  });

  test("returns all segments when none are active or cached", ({ expect }) => {
    const desiredSegments = [1, 2, 3, 4];
    const activeRequests = new Set<number>();
    const cachedSegments = new Set<number>();

    const queue = computeBufferQueue(
      desiredSegments,
      activeRequests,
      cachedSegments,
    );
    expect(queue).toEqual([1, 2, 3, 4]);
  });
});

describe("handleSeekTimeChange", () => {
  test("computes new queue and finds overlapping requests", ({
    mockVideoRendition,
    expect,
  }) => {
    // Use sync version with sync computeSegmentId for this test
    const syncComputeSegmentId = (
      timeMs: number,
      rendition: VideoRendition,
    ) => {
      return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
    };

    const currentState = {
      currentSeekTimeMs: 1000,
      activeRequests: new Set([2, 3]),
      cachedSegments: new Set([1]),
      requestQueue: [4, 5],
    };

    const result = handleSeekTimeChange(
      3000, // new seek time
      3000, // buffer 3 seconds
      mockVideoRendition,
      currentState,
      syncComputeSegmentId,
    );

    expect(result.newQueue).toEqual([4, 5, 6]); // segments 3 is active, 4,5,6 needed
    expect(result.overlappingRequests).toEqual([3]); // segment 3 is already being fetched
  });

  test("handles seek time change with no overlap", ({
    mockVideoRendition,
    expect,
  }) => {
    const syncComputeSegmentId = (
      timeMs: number,
      rendition: VideoRendition,
    ) => {
      return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
    };

    const currentState = {
      currentSeekTimeMs: 1000,
      activeRequests: new Set([1, 2]),
      cachedSegments: new Set<number>(),
      requestQueue: [],
    };

    const result = handleSeekTimeChange(
      10000, // seek far ahead
      2000, // buffer 2 seconds
      mockVideoRendition,
      currentState,
      syncComputeSegmentId,
    );

    expect(result.newQueue).toEqual([10, 11, 12]); // completely new segments
    expect(result.overlappingRequests).toEqual([]); // no overlap
  });

  test("handles backwards seek", ({ mockVideoRendition, expect }) => {
    const syncComputeSegmentId = (
      timeMs: number,
      rendition: VideoRendition,
    ) => {
      return Math.floor(timeMs / (rendition.segmentDurationMs || 1000));
    };

    const currentState = {
      currentSeekTimeMs: 5000,
      activeRequests: new Set([6, 7]),
      cachedSegments: new Set([5]),
      requestQueue: [],
    };

    const result = handleSeekTimeChange(
      2000, // seek backwards
      3000, // buffer 3 seconds
      mockVideoRendition,
      currentState,
      syncComputeSegmentId,
    );

    expect(result.newQueue).toEqual([2, 3, 4]); // segments 2,3,4,5 needed, 5 cached
    expect(result.overlappingRequests).toEqual([]); // no overlap with active requests
  });
});

describe("manageMediaBuffer (Video)", () => {
  test("manages buffer state successfully", async ({
    mockConfig,
    mockState,
    mockDeps,
    mockSignal,
    expect,
  }) => {
    const seekTimeMs = 3000;

    const newState = await manageMediaBuffer(
      seekTimeMs,
      mockConfig,
      mockState,
      10000, // durationMs
      mockSignal,
      mockDeps,
    );

    expect(newState.currentSeekTimeMs).toBe(seekTimeMs);
    expect(mockDeps.getRendition).toHaveBeenCalled();
    expect(mockDeps.fetchSegment).toHaveBeenCalledTimes(2); // maxParallelFetches = 2
  });

  test("respects maxParallelFetches limit", async ({
    mockState,
    mockDeps,
    mockSignal,
    expect,
  }) => {
    const config = {
      bufferDurationMs: 10000, // 10 seconds = 10 segments
      maxParallelFetches: 3,
      enableBuffering: true,
      enableContinuousBuffering: false, // Disable for predictable testing
    };

    await manageMediaBuffer(
      0,
      config,
      mockState,
      10000, // durationMs
      mockSignal,
      mockDeps,
    );

    expect(mockDeps.fetchSegment).toHaveBeenCalledTimes(3); // Should only fetch 3 despite needing 10
  });

  test("does nothing when buffering disabled", async ({
    mockState,
    mockDeps,
    mockSignal,
    expect,
  }) => {
    const config = {
      bufferDurationMs: 5000,
      maxParallelFetches: 2,
      enableBuffering: false,
    };

    const newState = await manageMediaBuffer(
      1000,
      config,
      mockState,
      10000, // durationMs
      mockSignal,
      mockDeps,
    );

    expect(newState).toBe(mockState); // Should return same state
    expect(mockDeps.fetchSegment).not.toHaveBeenCalled();
  });

  test("skips segments already in active requests", async ({
    mockConfig,
    mockDeps,
    mockSignal,
    expect,
  }) => {
    const stateWithActiveRequests = {
      currentSeekTimeMs: 0,
      activeRequests: new Set([1, 2]), // segments 1,2 already being fetched
      cachedSegments: new Set<number>(),
      requestQueue: [],
    };

    await manageMediaBuffer(
      1000, // seeks to time that would want segments 1,2,3,4,5
      mockConfig,
      stateWithActiveRequests,
      10000, // durationMs
      mockSignal,
      mockDeps,
    );

    // Should only fetch segments 3,4 (maxParallelFetches=2, skipping 1,2)
    expect(mockDeps.fetchSegment).toHaveBeenCalledWith(3, expect.any(Object));
    expect(mockDeps.fetchSegment).toHaveBeenCalledWith(4, expect.any(Object));
    expect(mockDeps.fetchSegment).toHaveBeenCalledTimes(2);
  });

  test("skips segments already cached", async ({
    mockConfig,
    mockDeps,
    mockSignal,
    expect,
  }) => {
    const stateWithCache = {
      currentSeekTimeMs: 0,
      activeRequests: new Set<number>(),
      cachedSegments: new Set([1, 3]), // segments 1,3 already cached
      requestQueue: [],
    };

    await manageMediaBuffer(
      1000, // seeks to time that would want segments 1,2,3,4,5
      mockConfig,
      stateWithCache,
      10000, // durationMs
      mockSignal,
      mockDeps,
    );

    // Should only fetch segments 2,4 (skipping cached 1,3)
    expect(mockDeps.fetchSegment).toHaveBeenCalledWith(2, expect.any(Object));
    expect(mockDeps.fetchSegment).toHaveBeenCalledWith(4, expect.any(Object));
    expect(mockDeps.fetchSegment).toHaveBeenCalledTimes(2);
  });
});

describe("makeVideoBufferTask", () => {
  beforeEach(() => {
    // MSW setup is now handled by test fixtures
  });

  afterEach(() => {
    const elements = document.querySelectorAll("test-media-video-buffer");
    for (const element of elements) {
      element.remove();
    }
    vi.restoreAllMocks();
  });

  test("creates task with correct configuration", ({ element, expect }) => {
    const task = makeVideoBufferTask(element);

    expect(task).toBeDefined();
    expect(task.status).toBe(TaskStatus.INITIAL);
    expect(task.value).toBeUndefined();
    expect(task.error).toBeUndefined();
  });

  test("task integrates with element seek time", ({ element, expect }) => {
    element.desiredSeekTimeMs = 5000;

    const task = makeVideoBufferTask(element);
    expect(task).toBeDefined();
    expect(task.status).toBe(TaskStatus.INITIAL);
  });
});

describe("Continuous Buffering", () => {
  test("enables continuous segment loading when enabled", async ({
    mockState,
    mockDeps,
    mockSignal,
    expect,
  }) => {
    const configWithContinuous = {
      bufferDurationMs: 10000, // 10 seconds = 10 segments
      maxParallelFetches: 2,
      enableBuffering: true,
      enableContinuousBuffering: true, // Enable continuous buffering
    };

    let fetchCount = 0;
    const mockFetchWithDelay = vi.fn().mockImplementation(() => {
      fetchCount++;
      return Promise.resolve(new ArrayBuffer(1000));
    });

    const mockDepsWithContinuous = {
      ...mockDeps,
      fetchSegment: mockFetchWithDelay,
    };

    await manageMediaBuffer(
      0,
      configWithContinuous,
      mockState,
      10000, // durationMs
      mockSignal,
      mockDepsWithContinuous,
    );

    // Should start with initial maxParallelFetches (2) and continue with more requests
    // Continuous buffering should fetch more segments as previous ones complete
    expect(mockFetchWithDelay).toHaveBeenCalledTimes(4); // More than initial batch due to continuous buffering
    expect(fetchCount).toBe(4);
  });

  test("disabled when flag is false", async ({
    mockState,
    mockDeps,
    mockSignal,
    expect,
  }) => {
    const configWithoutContinuous = {
      bufferDurationMs: 10000, // 10 seconds = 10 segments
      maxParallelFetches: 2,
      enableBuffering: true,
      enableContinuousBuffering: false, // Disable continuous buffering
    };

    await manageMediaBuffer(
      0,
      configWithoutContinuous,
      mockState,
      10000, // durationMs
      mockSignal,
      mockDeps,
    );

    // Should only fetch initial maxParallelFetches and stop
    expect(mockDeps.fetchSegment).toHaveBeenCalledTimes(2);
  });
});
