import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import noop from "../../../../utils/noop";
import sleep from "../../../../utils/sleep";
import TaskCanceller from "../../../../utils/task_canceller";
import ManifestFetcher from "../manifest_fetcher";

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */

const {
  mockConfigGetCurrent,
  mockFormatError,
  mockLogInfo,
  mockLogWarn,
  mockLogDebug,
  mockLoadManifest,
  mockParseManifest,
  mockScheduleRequestPromise,
  mockErrorSelector,
  MockManifest,
} = vi.hoisted(() => {
  class MockManifestImplem {
    expired: Promise<void> | null = null;
    lifetime: number | undefined = undefined;
    clockOffset: number | undefined = undefined;
    updateUrl: string | undefined = undefined;
    replace = vi.fn();
    update = vi.fn();
    getUrls = vi.fn(() => ["http://example.com/manifest"]);
  }
  return {
    mockConfigGetCurrent: vi.fn(),
    mockFormatError: vi.fn((e: unknown) => e),
    mockLogInfo: vi.fn(),
    mockLogWarn: vi.fn(),
    mockLogDebug: vi.fn(),
    mockLoadManifest: vi.fn(),
    mockParseManifest: vi.fn(),
    mockScheduleRequestPromise: vi.fn((fn: () => Promise<unknown>) => fn()),
    mockErrorSelector: vi.fn((e: unknown) => e),
    MockManifest: MockManifestImplem,
  };
});

vi.mock("../../../../config", () => ({ default: { getCurrent: mockConfigGetCurrent } }));
vi.mock("../../../../errors", () => ({ formatError: mockFormatError }));
vi.mock("../../../../log", () => ({
  default: { info: mockLogInfo, warn: mockLogWarn, debug: mockLogDebug },
}));
vi.mock("../../../../manifest/classes", () => ({ default: MockManifest }));
vi.mock("../../utils/error_selector", () => ({ default: mockErrorSelector }));
vi.mock("../../utils/schedule_request", () => ({
  scheduleRequestPromise: mockScheduleRequestPromise,
}));

const DEFAULT_CONFIG = {
  DEFAULT_REQUEST_TIMEOUT: 30_000,
  DEFAULT_CONNECTION_TIMEOUT: 10_000,
  DEFAULT_MAX_MANIFEST_REQUEST_RETRY: 4,
  INITIAL_BACKOFF_DELAY_BASE: { LOW_LATENCY: 100, REGULAR: 200 },
  MAX_BACKOFF_DELAY_BASE: { LOW_LATENCY: 3_000, REGULAR: 10_000 },
  MAX_CONSECUTIVE_MANIFEST_PARSING_IN_UNSAFE_MODE: 5,
  MIN_MANIFEST_PARSING_TIME_TO_ENTER_UNSAFE_MODE: 100,
  FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY: 2_000,
};

function makeSettings(overrides: Record<string, unknown> = {}) {
  return {
    lowLatencyMode: false,
    maxRetry: undefined,
    requestTimeout: undefined,
    connectionTimeout: undefined,
    minimumManifestUpdateInterval: 0,
    initialManifest: undefined,
    cmcdDataBuilder: null,
    ...overrides,
  };
}

function makePipelines(): any {
  return {
    transportName: "dash" as const,
    manifest: { loadManifest: mockLoadManifest, parseManifest: mockParseManifest },
  };
}

/** Resolved load response with sensible defaults */
function makeLoadResponse(overrides: Record<string, unknown> = {}) {
  return {
    responseData: {},
    size: 100,
    requestDuration: 10,
    sendingTime: 1_000,
    receivedTime: 1_010,
    ...overrides,
  };
}

describe("ManifestFetcher", () => {
  beforeEach(() => {
    mockConfigGetCurrent.mockReturnValue(DEFAULT_CONFIG);
    mockFormatError.mockImplementation((e: unknown) => e);
    mockErrorSelector.mockImplementation((e: unknown) => e);
    mockScheduleRequestPromise.mockImplementation((fn: () => Promise<unknown>) => fn());
  });

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

  describe("before start()", () => {
    it("scheduleManualRefresh is a no-op and does not throw", () => {
      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );
      expect(() =>
        fetcher.scheduleManualRefresh({
          enablePartialRefresh: false,
          delay: 0,
          canUseUnsafeMode: false,
        }),
      ).not.toThrow();
    });
  });

  describe("dispose()", () => {
    it("calls cancel with the given reason", () => {
      const mockCancel = vi.spyOn(TaskCanceller.prototype, "cancel");
      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );
      fetcher.dispose("test reason");
      expect(mockCancel).toHaveBeenCalledWith("test reason");
      mockCancel.mockRestore();
    });

    it("uses a default message when reason is undefined", () => {
      const mockCancel = vi.spyOn(TaskCanceller.prototype, "cancel");
      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );
      fetcher.dispose(undefined);
      expect(mockCancel).toHaveBeenCalledWith("ManifestFetcher dispose");
      mockCancel.mockRestore();
    });
  });

  describe("start() with a Manifest instance as initialManifest", () => {
    it("emits manifestReady immediately without fetching", async () => {
      const manifest = new MockManifest();
      const fetcher = new ManifestFetcher(
        undefined,
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );

      const onReady = vi.fn();
      fetcher.addEventListener("manifestReady", onReady);
      fetcher.start();
      await flushPromises();

      expect(onReady).toHaveBeenCalledWith(manifest);
      expect(mockLoadManifest).not.toHaveBeenCalled();
    });

    it("calling start() twice only emits manifestReady once", async () => {
      const manifest = new MockManifest();
      const fetcher = new ManifestFetcher(
        undefined,
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );

      const onReady = vi.fn();
      fetcher.addEventListener("manifestReady", onReady);
      fetcher.start();
      fetcher.start();
      await flushPromises();

      expect(onReady).toHaveBeenCalledTimes(1);
    });
  });

  describe("start() with raw initialManifest", () => {
    it("parses it and emits manifestReady (sync parser)", async () => {
      const manifest = new MockManifest();
      mockParseManifest.mockReturnValue({ manifest });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: { raw: true } }),
      );

      const onReady = vi.fn();
      fetcher.addEventListener("manifestReady", onReady);
      fetcher.start();
      await flushPromises();

      expect(mockParseManifest).toHaveBeenCalled();
      expect(onReady).toHaveBeenCalledWith(manifest);
      expect(mockLoadManifest).not.toHaveBeenCalled();
    });

    it("parses it and emits manifestReady (async parser)", async () => {
      const manifest = new MockManifest();
      mockParseManifest.mockReturnValue(Promise.resolve({ manifest }));

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: { raw: true } }),
      );

      const onReady = vi.fn();
      fetcher.addEventListener("manifestReady", onReady);
      fetcher.start();
      await flushPromises();

      expect(onReady).toHaveBeenCalledWith(manifest);
    });
  });

  describe("start() fetching from URL", () => {
    it("calls loadManifest with the first URL and emits manifestReady", async () => {
      const manifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );

      const onReady = vi.fn();
      fetcher.addEventListener("manifestReady", onReady);
      fetcher.start();
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledWith(
        "http://example.com/manifest",
        expect.objectContaining({ timeout: 30_000, connectionTimeout: 10_000 }),
        expect.anything(),
      );
      expect(onReady).toHaveBeenCalledWith(manifest);
    });

    it("passes undefined URL when no URLs are provided", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(undefined, makePipelines(), makeSettings());
      fetcher.start();
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledWith(
        undefined,
        expect.anything(),
        expect.anything(),
      );
    });

    it("sets timeout/connectionTimeout to undefined when given negative values", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ requestTimeout: -1, connectionTimeout: -1 }),
      );
      fetcher.start();
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ timeout: undefined, connectionTimeout: undefined }),
        expect.anything(),
      );
    });

    it("uses custom requestTimeout and connectionTimeout when positive", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ requestTimeout: 5_000, connectionTimeout: 2_000 }),
      );
      fetcher.start();
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ timeout: 5_000, connectionTimeout: 2_000 }),
        expect.anything(),
      );
    });

    it("includes CMCD payload when cmcdDataBuilder is set", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
      const cmcdPayload = { sid: "abc" };

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({
          cmcdDataBuilder: { getCmcdDataForManifest: vi.fn(() => cmcdPayload) },
        }),
      );
      fetcher.start();
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ cmcdPayload }),
        expect.anything(),
      );
    });

    it("emits error when loadManifest rejects", async () => {
      const loadError = new Error("network failure");
      mockLoadManifest.mockRejectedValue(loadError);

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );

      const onError = vi.fn();
      fetcher.addEventListener("error", onError);
      fetcher.start();
      await flushPromises();

      expect(onError).toHaveBeenCalledWith(loadError);
    });

    it("emits error when parseManifest throws", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      const parseError = new Error("parse failure");
      mockFormatError.mockReturnValue(parseError);
      mockParseManifest.mockImplementation(() => {
        throw parseError;
      });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );

      const onError = vi.fn();
      fetcher.addEventListener("error", onError);
      fetcher.start();
      await flushPromises();

      expect(onError).toHaveBeenCalledWith(parseError);
    });

    it("swallows the error when the canceller is already used at the time of failure", async () => {
      const mockIsUsed = vi.spyOn(TaskCanceller.prototype, "isUsed");
      mockIsUsed.mockImplementation(() => true);
      mockLoadManifest.mockRejectedValue(new Error("boom"));

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );

      const onError = vi.fn();
      fetcher.addEventListener("error", onError);
      fetcher.start();
      await flushPromises();

      expect(onError).not.toHaveBeenCalled();
      mockIsUsed.mockRestore();
    });
  });

  describe("backoff settings", () => {
    it("passes REGULAR delays in normal mode and uses config maxRetry as default", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ lowLatencyMode: false }),
      );
      fetcher.start();
      await flushPromises();

      expect(mockScheduleRequestPromise).toHaveBeenCalledWith(
        expect.any(Function),
        expect.objectContaining({ baseDelay: 200, maxDelay: 10_000, maxRetry: 4 }),
        expect.anything(),
      );
    });

    it("passes LOW_LATENCY delays when lowLatencyMode is true", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ lowLatencyMode: true }),
      );
      fetcher.start();
      await flushPromises();

      expect(mockScheduleRequestPromise).toHaveBeenCalledWith(
        expect.any(Function),
        expect.objectContaining({ baseDelay: 100, maxDelay: 3_000 }),
        expect.anything(),
      );
    });

    it("uses custom maxRetry from settings when provided", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ maxRetry: 10 }),
      );
      fetcher.start();
      await flushPromises();

      expect(mockScheduleRequestPromise).toHaveBeenCalledWith(
        expect.any(Function),
        expect.objectContaining({ maxRetry: 10 }),
        expect.anything(),
      );
    });
  });

  describe("updateContentUrls()", () => {
    it("does not trigger a load when refreshNow is false", async () => {
      const manifest = new MockManifest();
      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await flushPromises();

      fetcher.updateContentUrls(["http://new.com/manifest"], false);
      await sleep(0);
      await flushPromises();

      expect(mockLoadManifest).not.toHaveBeenCalled();
    });

    it("triggers an immediate refresh with the new URL when refreshNow is true", async () => {
      const manifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await flushPromises();

      fetcher.updateContentUrls(["http://new.com/manifest"], true);
      await sleep(0);
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledWith(
        "http://new.com/manifest",
        expect.anything(),
        expect.anything(),
      );
    });

    it("updates the stored manifest URLs", async () => {
      const manifest = new MockManifest();
      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await flushPromises();

      fetcher.updateContentUrls(["http://new.com/manifest"], false);

      expect(fetcher).toHaveProperty("_manifestUrls", ["http://new.com/manifest"]);
    });

    it("uses the new URL once for the next full refresh even when refreshNow is false", async () => {
      const manifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await flushPromises();

      fetcher.updateContentUrls(["http://new.com/manifest"], false);
      fetcher.scheduleManualRefresh({
        enablePartialRefresh: false,
        delay: 0,
        canUseUnsafeMode: false,
      });
      await sleep(0);
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledTimes(1);
      expect(mockLoadManifest).toHaveBeenLastCalledWith(
        "http://new.com/manifest",
        expect.anything(),
        expect.anything(),
      );
    });
  });

  describe("automatic refresh scheduling", () => {
    beforeEach(() => {
      vi.useFakeTimers();
    });
    afterEach(() => {
      vi.useRealTimers();
    });
    it("schedules a refresh after manifest.lifetime seconds", async () => {
      const manifest = new MockManifest();
      manifest.lifetime = 10;
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      vi.advanceTimersByTime(10_000);
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledTimes(1);
    });

    it("does not schedule a refresh when lifetime is undefined", async () => {
      const manifest = new MockManifest();
      manifest.lifetime = undefined;

      const fetcher = new ManifestFetcher(
        undefined,
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      vi.advanceTimersByTime(100_000);
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(mockLoadManifest).not.toHaveBeenCalled();
    });

    it("triggers a refresh when manifest.expired resolves", async () => {
      let resolveExpired!: () => void;
      const manifest = new MockManifest();
      manifest.expired = new Promise<void>((res) => {
        resolveExpired = res;
      });
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      resolveExpired();
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledTimes(1);
    });

    it("does not start a second refresh while one is already in-flight", async () => {
      const manifest = new MockManifest();
      // loader never resolves → refresh stays pending
      mockLoadManifest.mockReturnValue(
        new Promise(() => {
          /* pending */
        }),
      );

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      fetcher.scheduleManualRefresh({
        enablePartialRefresh: false,
        delay: 0,
        canUseUnsafeMode: false,
      });
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      fetcher.scheduleManualRefresh({
        enablePartialRefresh: false,
        delay: 0,
        canUseUnsafeMode: false,
      });
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledTimes(1);
    });

    it("respects minimumManifestUpdateInterval before firing a manual refresh", async () => {
      const manifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: new MockManifest() });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest, minimumManifestUpdateInterval: 5_000 }),
      );
      fetcher.start();
      await Promise.resolve();

      fetcher.scheduleManualRefresh({
        enablePartialRefresh: false,
        delay: 0,
        canUseUnsafeMode: false,
      });

      vi.advanceTimersByTime(4_999);
      expect(mockLoadManifest).not.toHaveBeenCalled();

      vi.advanceTimersByTime(1);
      expect(mockLoadManifest).toHaveBeenCalledTimes(1);
    });
  });

  describe("full vs partial refresh", () => {
    beforeEach(() => {
      vi.useFakeTimers();
    });
    afterEach(() => {
      vi.useRealTimers();
    });
    it("calls manifest.replace() on a full refresh", async () => {
      const manifest = new MockManifest();
      const refreshManifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: refreshManifest });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      fetcher.scheduleManualRefresh({
        enablePartialRefresh: false,
        delay: 0,
        canUseUnsafeMode: false,
      });
      vi.advanceTimersByTime(0);
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(manifest.replace).toHaveBeenCalledWith(refreshManifest);
      expect(manifest.update).not.toHaveBeenCalled();
    });

    it("calls manifest.update() on a partial refresh when updateUrl is set", async () => {
      const manifest = new MockManifest();
      manifest.updateUrl = "http://example.com/update";
      const refreshManifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: refreshManifest });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      fetcher.scheduleManualRefresh({
        enablePartialRefresh: true,
        delay: 0,
        canUseUnsafeMode: false,
      });
      vi.advanceTimersByTime(0);
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(manifest.replace).not.toHaveBeenCalled();
      expect(manifest.update).toHaveBeenCalledWith(refreshManifest);
    });

    it("falls back to a full refresh when manifest.update() throws, after a delay", async () => {
      const manifest = new MockManifest();
      manifest.updateUrl = "http://example.com/update";
      manifest.update.mockImplementationOnce(() => {
        throw new Error("conflict");
      });

      const refreshManifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(
        makeLoadResponse({ sendingTime: undefined, receivedTime: undefined }),
      );
      mockParseManifest.mockReturnValue({ manifest: refreshManifest });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      fetcher.scheduleManualRefresh({
        enablePartialRefresh: true,
        delay: 0,
        canUseUnsafeMode: false,
      });
      vi.advanceTimersByTime(0);
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      // Fallback full refresh fires after FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY
      vi.advanceTimersByTime(
        DEFAULT_CONFIG.FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY + 1,
      );
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(manifest.replace).toHaveBeenCalledWith(refreshManifest);
      expect(mockLogWarn).toHaveBeenCalledWith(
        "MF",
        expect.stringContaining("Attempt to update Manifest failed"),
        "Re-downloading the Manifest fully",
      );
    });

    it("uses the prioritized URL (from updateContentUrls) and forces a full refresh", async () => {
      const manifest = new MockManifest();
      manifest.updateUrl = "http://example.com/update";
      const refreshManifest = new MockManifest();
      mockLoadManifest.mockResolvedValue(makeLoadResponse());
      mockParseManifest.mockReturnValue({ manifest: refreshManifest });

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings({ initialManifest: manifest }),
      );
      fetcher.start();
      await Promise.resolve();

      fetcher.updateContentUrls(["http://prioritized.com/manifest"], false);
      fetcher.scheduleManualRefresh({
        enablePartialRefresh: true,
        delay: 0,
        canUseUnsafeMode: false,
      });
      vi.advanceTimersByTime(0);
      await vi.waitFor(noop, { timeout: 0 }); // real delay
      await flushPromises();

      expect(mockLoadManifest).toHaveBeenCalledWith(
        "http://prioritized.com/manifest",
        expect.anything(),
        expect.anything(),
      );
      expect(manifest.replace).toHaveBeenCalled();
      expect(manifest.update).not.toHaveBeenCalled();
    });
  });

  describe("warning events from the parser", () => {
    it("emits a warning for each entry reported via the onWarnings callback", async () => {
      mockLoadManifest.mockResolvedValue(makeLoadResponse());

      let capturedOnWarnings!: (w: Error[]) => void;
      mockParseManifest.mockImplementation(
        (_data: unknown, _opts: unknown, onWarnings: (w: Error[]) => void) => {
          capturedOnWarnings = onWarnings;
          return { manifest: new MockManifest() };
        },
      );

      const parseWarning = new Error("minor issue");
      mockFormatError.mockReturnValue(parseWarning);

      const fetcher = new ManifestFetcher(
        ["http://example.com/manifest"],
        makePipelines(),
        makeSettings(),
      );

      const onWarning = vi.fn();
      fetcher.addEventListener("warning", onWarning);
      fetcher.start();
      await flushPromises();

      capturedOnWarnings([parseWarning]);

      expect(onWarning).toHaveBeenCalledWith(parseWarning);
    });
  });
});

/** Flush microtask queue */
async function flushPromises(ticks = 100) {
  for (let i = 0; i < ticks; i++) {
    await Promise.resolve();
  }
}
