import { describe, beforeEach, afterEach, it, expect, vi } from "vitest";
import globalScope from "../../../../../utils/global_scope";
import probeDecodingInfos from "../../probers/decodingInfo";
import type { IMediaConfiguration } from "../../types";

const oldNavigator = globalScope.navigator;

/**
 * Stub decodingInfo API to resolve.
 * @param {boolean} isSupported
 * @param {undefined|boolean} mustReject
 */
function stubDecodingInfo(isSupported: boolean, mustReject?: boolean) {
  const decodingInfoStub = vi.fn(() => {
    if (mustReject === true) {
      return Promise.reject();
    } else {
      return Promise.resolve({
        supported: isSupported,
      });
    }
  });

  const mockMediaCapabilities = {
    decodingInfo: decodingInfoStub,
  } as unknown as MediaCapabilities;

  Object.defineProperty(navigator, "mediaCapabilities", {
    get() {
      return mockMediaCapabilities;
    },
  });
  return decodingInfoStub;
}

describe("MediaCapabilitiesProber probers - decodingInfo", () => {
  beforeEach(() => {
    vi.resetModules();

    // Ugly trick to authorize the resetting of the `navigator` property:
    //   1. We redefine what window.navigator points to so it can be modified.
    //      We don't care about its value here.
    //   2. We're now able to remove it from `window`.
    //   3. We now redefine it as an empty object.
    Object.defineProperty(globalScope, "navigator", {});
    // @ts-expect-error: part of the aforementioned trick
    delete globalScope.navigator;
    // @ts-expect-error: part of the aforementioned trick
    globalScope.navigator = {};
  });

  afterEach(() => {
    globalScope.navigator = oldNavigator;
  });

  it("should throw if no video and audio config", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration = {
      type: "media-source" as const,
    };

    let thrownException = false;
    try {
      await probeDecodingInfos(configuration);
    } catch (error) {
      const message = error instanceof Error ? error.message : "";
      thrownException = true;
      expect(message).toEqual("Not enough arguments for calling mediaCapabilites.");
      expect(decodingInfoStub).not.toHaveBeenCalled();
    }
    expect(thrownException).toEqual(true);
  });

  it("should throw if incomplete video config", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration = {
      type: "media-source" as const,
      video: {
        contentType: "video/wmv",
      },
    };

    let thrownException = false;
    try {
      await probeDecodingInfos(configuration);
    } catch (error) {
      const message = error instanceof Error ? error.message : "";
      thrownException = true;
      expect(message).toEqual("Not enough arguments for calling mediaCapabilites.");
      expect(decodingInfoStub).not.toHaveBeenCalled();
    }
    expect(thrownException).toEqual(true);
  });

  it("should throw if incomplete audio config", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration = {
      type: "media-source" as const,
      audio: {
        contentType: "audio/wma",
      },
    };

    let thrownException = false;
    try {
      await probeDecodingInfos(configuration);
    } catch (error) {
      const message = error instanceof Error ? error.message : "";
      thrownException = true;
      expect(message).toEqual("Not enough arguments for calling mediaCapabilites.");
      expect(decodingInfoStub).not.toHaveBeenCalled();
    }
    expect(thrownException).toEqual(true);
    return probeDecodingInfos(configuration).catch(({ message }: { message: string }) => {
      expect(message).toEqual("Not enough arguments for calling mediaCapabilites.");
      expect(decodingInfoStub).not.toHaveBeenCalled();
    });
  });

  it("should throw if no type in config", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration = {
      audio: {
        contentType: "audio/wma",
      },
    };
    let thrownException = false;
    try {
      await probeDecodingInfos(configuration);
    } catch (error) {
      const message = error instanceof Error ? error.message : "";
      thrownException = true;
      expect(message).toEqual("Not enough arguments for calling mediaCapabilites.");
      expect(decodingInfoStub).not.toHaveBeenCalled();
    }
    expect(thrownException).toEqual(true);
  });

  it("should throw if empty config", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration = {};

    let thrownException = false;
    try {
      await probeDecodingInfos(configuration);
    } catch (error) {
      const message = error instanceof Error ? error.message : "";
      thrownException = true;
      expect(message).toEqual("Not enough arguments for calling mediaCapabilites.");
      expect(decodingInfoStub).not.toHaveBeenCalled();
    }
    expect(thrownException).toEqual(true);
  });

  it("should throw if API mediaCapabilities not available", async () => {
    await expect(probeDecodingInfos({})).rejects.toThrowError(
      "navigator.mediaCapabilites.decodingInfo is not available",
    );
  });

  it("should throw if API decodingInfo not available", async () => {
    Object.defineProperty(navigator, "mediaCapabilities", {
      get() {
        return {};
      },
    });
    await expect(probeDecodingInfos({})).rejects.toThrowError(
      "navigator.mediaCapabilites.decodingInfo is not available",
    );
  });

  it("should resolve with `Supported` if decodingInfo supports (video only)", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration: IMediaConfiguration = {
      type: "media-source",
      video: {
        contentType: "audio/wma",
        bitrate: 5000,
        framerate: "24",
        height: 1080,
        width: 1920,
      },
    };

    const res = await probeDecodingInfos(configuration);
    expect(res).toBe("Supported");
    expect(decodingInfoStub).toHaveBeenCalledTimes(1);
  });

  it("should resolve with `Supported` if decodingInfo supports (audio only)", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration: IMediaConfiguration = {
      type: "media-source",
      audio: {
        contentType: "audio/wma",
        bitrate: 5000,
        samplerate: 44100,
        channels: "5.1",
      },
    };

    const res = await probeDecodingInfos(configuration);
    expect(res).toBe("Supported");
    expect(decodingInfoStub).toHaveBeenCalledTimes(1);
  });

  it("should resolve with `Supported` if decodingInfo supports video + audio", async () => {
    const decodingInfoStub = stubDecodingInfo(true);
    const configuration: IMediaConfiguration = {
      type: "media-source",
      video: {
        contentType: "audio/wma",
        bitrate: 5000,
        framerate: "24",
        height: 1080,
        width: 1920,
      },
      audio: {
        contentType: "audio/wma",
        bitrate: 5000,
        samplerate: 44100,
        channels: "5.1",
      },
    };

    const res = await probeDecodingInfos(configuration);
    expect(res).toBe("Supported");
    expect(decodingInfoStub).toHaveBeenCalledTimes(1);
  });

  it("should return `NotSupported` if no decodingInfo support (video only)", async () => {
    const decodingInfoStub = stubDecodingInfo(false);
    const configuration: IMediaConfiguration = {
      type: "media-source",
      video: {
        contentType: "audio/wma",
        bitrate: 5000,
        framerate: "24",
        height: 1080,
        width: 1920,
      },
    };

    const res = await probeDecodingInfos(configuration);
    expect(res).toBe("NotSupported");
    expect(decodingInfoStub).toHaveBeenCalledTimes(1);
  });

  it("should return `NotSupported` if no decodingInfo support (audio only)", async () => {
    const decodingInfoStub = stubDecodingInfo(false);
    const configuration: IMediaConfiguration = {
      type: "media-source",
      audio: {
        contentType: "audio/wma",
        bitrate: 5000,
        samplerate: 44100,
        channels: "5.1",
      },
    };

    const res = await probeDecodingInfos(configuration);
    expect(res).toBe("NotSupported");
    expect(decodingInfoStub).toHaveBeenCalledTimes(1);
  });

  it("should return `NotSupported` if no decodingInfo support", async () => {
    const decodingInfoStub = stubDecodingInfo(false);
    const configuration: IMediaConfiguration = {
      type: "media-source",
      video: {
        contentType: "audio/wma",
        bitrate: 5000,
        framerate: "24",
        height: 1080,
        width: 1920,
      },
      audio: {
        contentType: "audio/wma",
        bitrate: 5000,
        samplerate: 44100,
        channels: "5.1",
      },
    };

    const res = await probeDecodingInfos(configuration);
    expect(res).toBe("NotSupported");
    expect(decodingInfoStub).toHaveBeenCalledTimes(1);
  });

  it("should resolve with `NotSupported` if decodingInfo throws", async () => {
    const decodingInfoStub = stubDecodingInfo(true, true);
    const configuration: IMediaConfiguration = {
      type: "media-source",
      video: {
        contentType: "audio/wma",
        bitrate: 5000,
        framerate: "24",
        height: 1080,
        width: 1920,
      },
      audio: {
        contentType: "audio/wma",
        bitrate: 5000,
        samplerate: 44100,
        channels: "5.1",
      },
    };

    const res = await probeDecodingInfos(configuration);
    expect(res).toBe("NotSupported");
    expect(decodingInfoStub).toHaveBeenCalledTimes(1);
  });
});
