import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import configHandler from "../../../config";
import {
  DummyAdaptation,
  DummyPeriod,
  DummyRepresentation,
  DummyManifest,
  createSegment,
} from "../../../manifest/classes/__tests__/mocks";
import { DummyObservationPosition } from "../../../playback_observer/__tests__/mocks";
import { makeMockedClass } from "../../../utils/test-utils";
import type { IPlaybackConditionsInfo } from "../network_analyzer";
import NetworkAnalyzer, { estimateRequestBandwidth } from "../network_analyzer";
import type BandwidthEstimator from "../utils/bandwidth_estimator";
import type {
  IPendingRequestStoreProgress,
  IRequestInfo,
} from "../utils/pending_requests_store";

vi.mock("../../../log", () => ({
  default: {
    info: vi.fn(),
  },
}));

function createMockRequest(
  segmentTime: number,
  segmentDuration: number,
  requestTimestamp: number,
  progress: Array<Partial<IPendingRequestStoreProgress>> = [],
  complete = true,
): IRequestInfo {
  return {
    content: {
      segment: createSegment({
        time: segmentTime,
        duration: segmentDuration,
        complete,
      }),
      adaptation: new DummyAdaptation(),
      period: new DummyPeriod(),
      representation: new DummyRepresentation(),
      manifest: new DummyManifest(),
    },
    requestTimestamp,
    progress: progress.map((p) => {
      return {
        size: 1000,
        timestamp: 1000,
        totalSize: 10000,
        duration: 2,
        id: "toto",
        ...p,
      };
    }),
  };
}

function createMockPlaybackInfo(
  bufferGap: number,
  position: number,
  speed = 1,
  duration = 100,
): IPlaybackConditionsInfo {
  return {
    bufferGap,
    position: new DummyObservationPosition({
      getWanted: () => position,
      getPolled: () => position,
    }),
    speed,
    duration,
  };
}

function createMockBandwidthEstimator(estimate?: number): BandwidthEstimator {
  const DummyBandwidthEstimator = makeMockedClass<BandwidthEstimator>(
    {
      getEstimate: vi.fn(() => estimate),
      reset: vi.fn(),
      addSample: vi.fn(),
    },
    {},
  );
  return new DummyBandwidthEstimator();
}
beforeEach(() => {
  const originalConfig = configHandler.getCurrent();
  vi.spyOn(configHandler, "getCurrent").mockReturnValue({
    ...originalConfig,
    ABR_STARVATION_GAP: {
      DEFAULT: 2,
      LOW_LATENCY: 1,
    },
    OUT_OF_STARVATION_GAP: {
      DEFAULT: 3,
      LOW_LATENCY: 1.5,
    },
    ABR_STARVATION_FACTOR: {
      DEFAULT: 0.72,
      LOW_LATENCY: 0.8,
    },
    ABR_REGULAR_FACTOR: {
      DEFAULT: 0.8,
      LOW_LATENCY: 0.85,
    },
    ABR_STARVATION_DURATION_DELTA: 5,
  });
});
afterEach(() => {
  vi.resetAllMocks();
});

function createProgressEvents(
  baseTime: number,
  totalSize: number,
  stepSize: number,
  stepDurationMs: number,
): Array<{ size: number; timestamp: number; totalSize: number }> {
  const progress = [{ size: 0, timestamp: baseTime, totalSize }];
  let size = 0;
  let timestamp = baseTime;
  while (size < totalSize) {
    size = Math.min(size + stepSize, totalSize);
    timestamp += stepDurationMs;
    progress.push({ size, timestamp, totalSize });
  }
  return progress;
}

describe("estimateRequestBandwidth", () => {
  it("should return undefined if progress events are less than 5", () => {
    // eslint-disable-next-line no-restricted-properties
    const request = createMockRequest(0, 4, performance.now(), [
      { size: 1000, timestamp: 1000, totalSize: 10000 },
      { size: 2000, timestamp: 2000, totalSize: 10000 },
    ]);
    expect(estimateRequestBandwidth(request)).toBeUndefined();
  });

  it("should calculate bandwidth estimate from progress events", () => {
    const baseTime = 1000;
    const request = createMockRequest(0, 4, baseTime, [
      { size: 0, timestamp: baseTime, totalSize: 10000 },
      { size: 1000, timestamp: baseTime + 100, totalSize: 10000 },
      { size: 2000, timestamp: baseTime + 200, totalSize: 10000 },
      { size: 3000, timestamp: baseTime + 300, totalSize: 10000 },
      { size: 4000, timestamp: baseTime + 400, totalSize: 10000 },
      { size: 5000, timestamp: baseTime + 500, totalSize: 10000 },
    ]);
    const estimate = estimateRequestBandwidth(request);
    expect(estimate).toBeDefined();
    expect(typeof estimate).toBe("number");
    expect(estimate).toBeGreaterThan(50000);
  });

  it("should handle varying download speeds", () => {
    const baseTime = 1000;
    const request = createMockRequest(0, 4, baseTime, [
      { size: 0, timestamp: baseTime, totalSize: 20000 },
      { size: 2000, timestamp: baseTime + 100, totalSize: 20000 },
      { size: 4000, timestamp: baseTime + 200, totalSize: 20000 },
      { size: 5000, timestamp: baseTime + 400, totalSize: 20000 }, // Slower
      { size: 6000, timestamp: baseTime + 600, totalSize: 20000 }, // Slower
      { size: 7000, timestamp: baseTime + 800, totalSize: 20000 },
    ]);
    const estimate = estimateRequestBandwidth(request);
    expect(estimate).toBeDefined();
    expect(estimate).toBeGreaterThan(50000);
  });
});

describe("NetworkAnalyzer", () => {
  describe("constructor", () => {
    it("should initialize with default mode configuration", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      expect(analyzer).toBeDefined();
    });

    it("should initialize with low latency mode configuration", () => {
      const analyzer = new NetworkAnalyzer(1000000, true);
      expect(analyzer).toBeDefined();
    });
  });

  describe("getBandwidthEstimate", () => {
    it("should return initial bitrate when no bandwidth estimate is available", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 0);
      const bandwidthEstimator = createMockBandwidthEstimator(undefined);

      const result = analyzer.getBandwidthEstimate(
        playbackInfo,
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      expect(result.bitrateChosen).toBe(1000000);
    });

    it("should apply regular factor to bandwidth estimate in normal mode", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 0);
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      const result = analyzer.getBandwidthEstimate(
        playbackInfo,
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Should apply ABR_REGULAR_FACTOR.DEFAULT (0.8)
      expect(result.bitrateChosen).toBe(2000000 * 0.8);
      expect(result.bandwidthEstimate).toBe(2000000);
    });

    it("should enter starvation mode when buffer gap is low", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(1, 10); // Low buffer gap
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      const result = analyzer.getBandwidthEstimate(
        playbackInfo,
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Should apply ABR_STARVATION_FACTOR.DEFAULT (0.72)
      expect(result.bitrateChosen).toBe(2000000 * 0.72);
    });

    it("should exit starvation mode when buffer gap increases", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      // Enter starvation mode
      analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(1, 10),
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Exit starvation mode
      const result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(5, 10),
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Should apply regular factor again (0.8)
      expect(result.bitrateChosen).toBe(2000000 * 0.8);
    });

    it("should adjust bitrate based on playback speed", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 0, 2); // 2x speed
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      const result = analyzer.getBandwidthEstimate(
        playbackInfo,
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Should divide by speed: (2000000 * 0.8) / 2
      expect(result.bitrateChosen).toBe((2000000 * 0.8) / 2);
    });

    it("should use last estimated bitrate when no new estimate available", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 0);
      const bandwidthEstimator = createMockBandwidthEstimator(undefined);

      const result = analyzer.getBandwidthEstimate(
        playbackInfo,
        bandwidthEstimator,
        null,
        [],
        1500000, // Last estimated bitrate
      );

      expect(result.bitrateChosen).toBe(1500000 * 0.8);
    });

    it("should exit starvation mode near end of content", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      // Enter starvation mode
      analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(1, 10, 1, 100),
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Near end of content (position + buffer gap close to duration)
      const result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(1, 96, 1, 100), // 96 + 1 = 97, close to 100
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Should exit starvation mode and use regular factor
      expect(result.bitrateChosen).toBe(2000000 * 0.8);
    });

    it("should allow emergency estimate before 12s for short segments", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const bandwidthEstimator = createMockBandwidthEstimator(undefined);
      const resetSpy = vi.spyOn(bandwidthEstimator, "reset");
      const requestStart = 1000;
      const now = 7000;
      const performanceNowSpy = vi.spyOn(performance, "now").mockReturnValue(now);
      const currentRepresentation = new DummyRepresentation({
        bitrate: 1_200_000,
      });
      const currentRequests = [
        createMockRequest(
          10,
          2,
          requestStart,
          createProgressEvents(requestStart, 1_000_000, 100_000, 100),
          true,
        ),
      ];

      const result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(1, 9),
        bandwidthEstimator,
        currentRepresentation,
        currentRequests,
        1_100_000,
      );

      expect(result.bandwidthEstimate).toBeDefined();
      expect(result.bitrateChosen).toBeLessThan(currentRepresentation.bitrate);
      expect(resetSpy).toHaveBeenCalled();
      performanceNowSpy.mockRestore();
      resetSpy.mockRestore();
    });

    it("should handle infinite buffer gap", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(Infinity, 0);
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      const result = analyzer.getBandwidthEstimate(
        playbackInfo,
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Should treat infinite as 0 and apply starvation factor
      expect(result.bitrateChosen).toBe(2000000 * 0.72);
    });

    it("should use different thresholds in low latency mode", () => {
      const analyzer = new NetworkAnalyzer(1000000, true);
      const playbackInfo = createMockPlaybackInfo(0.5, 10); // Would enter starvation in low latency
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      const result = analyzer.getBandwidthEstimate(
        playbackInfo,
        bandwidthEstimator,
        null,
        [],
        undefined,
      );

      // Should use low latency starvation factor (0.8)
      expect(result.bitrateChosen).toBe(2000000 * 0.8);
    });
  });

  describe("isUrgent", () => {
    it("should return true when no current representation", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 0);

      const result = analyzer.isUrgent(500000, null, [], playbackInfo);

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

    it("should return false when bitrate is higher than current representation", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 0);
      const currentRepresentation = new DummyRepresentation({
        bitrate: 1000000,
      });

      const result = analyzer.isUrgent(2000000, currentRepresentation, [], playbackInfo);

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

    it("should return true in low latency mode when switching to lower bitrate", () => {
      const analyzer = new NetworkAnalyzer(1000000, true);
      const playbackInfo = createMockPlaybackInfo(5, 0);
      const currentRepresentation = new DummyRepresentation({
        bitrate: 2000000,
      });

      const result = analyzer.isUrgent(1000000, currentRepresentation, [], playbackInfo);

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

    it("should return true when no pending requests for lower bitrate", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 10);
      const currentRepresentation = new DummyRepresentation({
        bitrate: 2000000,
      });

      const result = analyzer.isUrgent(1000000, currentRepresentation, [], playbackInfo);

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

    it("should evaluate urgency based on pending request progress", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      // eslint-disable-next-line no-restricted-properties
      const now = performance.now();
      const playbackInfo = createMockPlaybackInfo(2, 10);
      const currentRepresentation = new DummyRepresentation({
        bitrate: 2000000,
      });

      const request = createMockRequest(10, 4, now - 1000, [
        { size: 0, timestamp: now - 1000, totalSize: 10000 },
        { size: 2000, timestamp: now - 800, totalSize: 10000 },
        { size: 4000, timestamp: now - 600, totalSize: 10000 },
        { size: 6000, timestamp: now - 400, totalSize: 10000 },
        { size: 8000, timestamp: now - 200, totalSize: 10000 },
      ]);

      const result = analyzer.isUrgent(
        1000000,
        currentRepresentation,
        [request],
        playbackInfo,
      );

      expect(typeof result).toBe("boolean");
    });

    it("should handle requests with zero duration segments", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(5, 10);
      const currentRepresentation = new DummyRepresentation({
        bitrate: 2000000,
      });

      // eslint-disable-next-line no-restricted-properties
      const request = createMockRequest(10, 0, performance.now()); // Zero duration

      const result = analyzer.isUrgent(
        1000000,
        currentRepresentation,
        [request],
        playbackInfo,
      );

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

    it("should handle infinite buffer gap in urgency check", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const playbackInfo = createMockPlaybackInfo(Infinity, 10);
      const currentRepresentation = new DummyRepresentation({
        bitrate: 2000000,
      });

      const result = analyzer.isUrgent(1000000, currentRepresentation, [], playbackInfo);

      expect(typeof result).toBe("boolean");
    });
  });

  describe("integration scenarios", () => {
    it("should handle typical playback progression", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const bandwidthEstimator = createMockBandwidthEstimator(2000000);

      // Initial state - good buffer
      let result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(10, 0),
        bandwidthEstimator,
        null,
        [],
        undefined,
      );
      expect(result.bitrateChosen).toBe(2000000 * 0.8);

      // Buffer depleting - entering starvation
      result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(1.5, 20),
        bandwidthEstimator,
        new DummyRepresentation({ bitrate: 1600000 }),
        [],
        result.bitrateChosen,
      );
      expect(result.bitrateChosen).toBe(2000000 * 0.72);

      // Buffer recovering - exiting starvation
      result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(4, 30),
        bandwidthEstimator,
        new DummyRepresentation({ bitrate: 1440000 }),
        [],
        result.bitrateChosen,
      );
      expect(result.bitrateChosen).toBe(2000000 * 0.8);
    });

    it("should handle bandwidth fluctuations", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);

      // Start with good bandwidth
      let estimator = createMockBandwidthEstimator(3000000);
      let result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(5, 0),
        estimator,
        null,
        [],
        undefined,
      );
      expect(result.bitrateChosen).toBe(3000000 * 0.8);

      // Bandwidth drops
      estimator = createMockBandwidthEstimator(1000000);
      result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(5, 10),
        estimator,
        new DummyRepresentation({ bitrate: 2400000 }),
        [],
        result.bitrateChosen,
      );
      expect(result.bitrateChosen).toBe(1000000 * 0.8);

      // Bandwidth recovers
      estimator = createMockBandwidthEstimator(2500000);
      result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(5, 20),
        estimator,
        new DummyRepresentation({ bitrate: 800000 }),
        [],
        result.bitrateChosen,
      );
      expect(result.bitrateChosen).toBe(2500000 * 0.8);
    });

    it("should handle speed changes during playback", () => {
      const analyzer = new NetworkAnalyzer(1000000, false);
      const estimator = createMockBandwidthEstimator(2000000);

      // Normal speed
      let result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(5, 0, 1),
        estimator,
        null,
        [],
        undefined,
      );
      const normalBitrate = result.bitrateChosen;

      // 2x speed - should halve the bitrate
      result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(5, 10, 2),
        estimator,
        new DummyRepresentation({ bitrate: normalBitrate }),
        [],
        result.bitrateChosen,
      );
      expect(result.bitrateChosen).toBe(normalBitrate / 2);

      // 0.5x speed - keep regular bitrate
      result = analyzer.getBandwidthEstimate(
        createMockPlaybackInfo(5, 20, 0.5),
        estimator,
        new DummyRepresentation({ bitrate: normalBitrate }),
        [],
        result.bitrateChosen,
      );
      expect(result.bitrateChosen).toBe(normalBitrate);
    });
  });
});
