import { describe, beforeEach, afterEach, it, expect, vi } from "vitest";
import type { IParsedAdaptation, IParsedRepresentation } from "../../../parsers/manifest";
import type {
  IRepresentationContext,
  IRepresentationFilterRepresentation,
} from "../../../public_types";
import type IAdaptation from "../adaptation";
import CodecSupportCache from "../codec_support_cache";
import type { IRepresentationIndex } from "../representation_index";

const minimalRepresentationIndex: IRepresentationIndex = {
  getInitSegment() {
    return null;
  },
  getSegments() {
    return [];
  },
  shouldRefresh() {
    return false;
  },
  getFirstAvailablePosition(): undefined {
    return;
  },
  getLastAvailablePosition(): undefined {
    return;
  },
  getEnd(): undefined {
    return;
  },
  checkDiscontinuity() {
    return null;
  },
  isSegmentStillAvailable(): undefined {
    return;
  },
  canBeOutOfSyncError(): true {
    return true;
  },
  isStillAwaitingFutureSegments() {
    return false;
  },
  isInitialized(): true {
    return true;
  },
  awaitSegmentBetween(): undefined {
    return;
  },
  initialize() {
    /* noop */
  },
  addPredictedSegments() {
    /* noop */
  },
  getTargetSegmentDuration() {
    return undefined;
  },
  _replace() {
    /* noop */
  },
  _update() {
    /* noop */
  },
};
const mockDefaultRepresentationImpl = vi.fn((arg: IParsedRepresentation) => {
  return {
    bitrate: arg.bitrate,
    id: arg.id,
    isSupported: true,
    isPlayable() {
      return true;
    },
    index: arg.index,
  };
});

describe("Manifest - Adaptation", () => {
  beforeEach(() => {
    vi.resetModules();
  });
  afterEach(() => {
    mockDefaultRepresentationImpl.mockClear();
  });

  it("should be able to create a minimal Adaptation", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;
    const args: IParsedAdaptation = { id: "12", representations: [], type: "video" };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation = new Adaptation(args, codecSupportCache);
    expect(adaptation.id).toBe("12");
    expect(adaptation.representations).toEqual([]);
    expect(adaptation.type).toBe("video");
    expect(adaptation.isAudioDescription).toBe(undefined);
    expect(adaptation.isClosedCaption).toBe(undefined);
    expect(adaptation.language).toBe(undefined);
    expect(adaptation.normalizedLanguage).toBe(undefined);
    expect(adaptation.manuallyAdded).toBe(false);
    expect(adaptation.getRepresentation("")).toBe(undefined);

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

  it("should normalize a given language", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));
    const mockNormalize = vi.fn((lang: string) => lang + "foo");
    vi.doMock("../../../utils/languages", () => ({
      default: mockNormalize,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;
    const args1: IParsedAdaptation = {
      id: "12",
      representations: [],
      language: "fr",
      type: "video" as const,
    };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation1 = new Adaptation(args1, codecSupportCache);
    expect(adaptation1.language).toBe("fr");
    expect(adaptation1.normalizedLanguage).toBe("frfoo");
    expect(mockNormalize).toHaveBeenCalledTimes(1);
    expect(mockNormalize).toHaveBeenCalledWith("fr");
    mockNormalize.mockClear();

    const args2: IParsedAdaptation = {
      id: "12",
      representations: [],
      language: "toto",
      type: "video" as const,
    };
    const adaptation2 = new Adaptation(args2, codecSupportCache);
    expect(adaptation2.language).toBe("toto");
    expect(adaptation2.normalizedLanguage).toBe("totofoo");
    expect(mockNormalize).toHaveBeenCalledTimes(1);
    expect(mockNormalize).toHaveBeenCalledWith("toto");
  });

  it("should not call normalize if no language is given", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));
    const mockNormalize = vi.fn((lang: string) => lang + "foo");
    vi.doMock("../../../utils/languages", () => ({
      default: mockNormalize,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;
    const args1: IParsedAdaptation = { id: "12", representations: [], type: "video" };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation1 = new Adaptation(args1, codecSupportCache);
    expect(adaptation1.language).toBe(undefined);
    expect(adaptation1.normalizedLanguage).toBe(undefined);
    expect(mockNormalize).not.toHaveBeenCalled();
  });

  it("should create and sort the corresponding Representations", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;
    const rep1 = {
      bitrate: 10,
      id: "rep1",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep2 = {
      bitrate: 30,
      id: "rep2",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep3 = {
      bitrate: 20,
      id: "rep3",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const representations = [rep1, rep2, rep3];
    const args: IParsedAdaptation = { id: "12", representations, type: "text" };

    const codecSupportCache = new CodecSupportCache([]);
    const adaptation = new Adaptation(args, codecSupportCache);
    const parsedRepresentations = adaptation.representations;
    expect(mockDefaultRepresentationImpl).toHaveBeenCalledTimes(3);
    expect(mockDefaultRepresentationImpl).toHaveBeenNthCalledWith(
      1,
      rep1,
      "text",
      codecSupportCache,
    );
    expect(mockDefaultRepresentationImpl).toHaveBeenNthCalledWith(
      2,
      rep2,
      "text",
      codecSupportCache,
    );
    expect(mockDefaultRepresentationImpl).toHaveBeenNthCalledWith(
      3,
      rep3,
      "text",
      codecSupportCache,
    );

    expect(parsedRepresentations.length).toBe(3);
    expect(parsedRepresentations[0].id).toEqual("rep1");
    expect(parsedRepresentations[1].id).toEqual("rep3");
    expect(parsedRepresentations[2].id).toEqual("rep2");

    expect(adaptation.getRepresentation("rep2")?.bitrate).toEqual(30);
  });

  it("should execute the representationFilter if given", async () => {
    const mockRepresentation = vi.fn((arg: IParsedRepresentation) => {
      return {
        bitrate: arg.bitrate,
        id: arg.id,
        isSupported: arg.id !== "rep4",
        isPlayable() {
          return true;
        },
        index: arg.index,
      };
    });

    vi.doMock("../representation", () => ({
      default: mockRepresentation,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;
    const rep1 = {
      bitrate: 10,
      id: "rep1",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep2 = {
      bitrate: 20,
      id: "rep2",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep3 = {
      bitrate: 30,
      id: "rep3",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep4 = {
      bitrate: 40,
      id: "rep4",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep5 = {
      bitrate: 50,
      id: "rep5",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep6 = {
      bitrate: 60,
      id: "rep6",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const representations = [rep1, rep2, rep3, rep4, rep5, rep6];

    const representationFilter = vi.fn(
      (
        representation: IRepresentationFilterRepresentation,
        adaptationInfos: IRepresentationContext,
      ) => {
        if (
          adaptationInfos.language === "fr" &&
          representation.bitrate !== undefined &&
          representation.bitrate < 40
        ) {
          return false;
        }
        return true;
      },
    );
    const args: IParsedAdaptation = {
      id: "12",
      language: "fr",
      representations,
      type: "text" as const,
    };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation = new Adaptation(args, codecSupportCache, { representationFilter });

    const parsedRepresentations = adaptation.representations;
    expect(representationFilter).toHaveBeenCalledTimes(6);
    expect(parsedRepresentations.length).toBe(3);

    expect(parsedRepresentations[0].id).toEqual("rep4");
    expect(parsedRepresentations[1].id).toEqual("rep5");
    expect(parsedRepresentations[2].id).toEqual("rep6");

    expect(adaptation.getRepresentation("rep2")).toBe(undefined);
    expect(adaptation.getRepresentation("rep4")?.id).toEqual("rep4");
  });

  it("should set an isDub value if one", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));
    const mockNormalize = vi.fn((lang: string) => lang + "foo");
    vi.doMock("../../../utils/languages", () => ({
      default: mockNormalize,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;

    const args1: IParsedAdaptation = {
      id: "12",
      representations: [],
      isDub: false,
      type: "video",
    };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation1 = new Adaptation(args1, codecSupportCache);
    expect(adaptation1.language).toBe(undefined);
    expect(adaptation1.normalizedLanguage).toBe(undefined);
    expect(adaptation1.isDub).toEqual(false);
    expect(mockNormalize).not.toHaveBeenCalled();

    const args2: IParsedAdaptation = {
      id: "12",
      representations: [],
      isDub: true,
      type: "video",
    };
    const adaptation2 = new Adaptation(args2, codecSupportCache);
    expect(adaptation2.language).toBe(undefined);
    expect(adaptation2.normalizedLanguage).toBe(undefined);
    expect(adaptation2.isDub).toEqual(true);
    expect(mockNormalize).not.toHaveBeenCalled();
  });

  it("should set an isClosedCaption value if one", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));
    const mockNormalize = vi.fn((lang: string) => lang + "foo");
    vi.doMock("../../../utils/languages", () => ({
      default: mockNormalize,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;

    const args1: IParsedAdaptation = {
      id: "12",
      representations: [],
      closedCaption: false,
      type: "video",
    };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation1 = new Adaptation(args1, codecSupportCache);
    expect(adaptation1.language).toBe(undefined);
    expect(adaptation1.normalizedLanguage).toBe(undefined);
    expect(adaptation1.isClosedCaption).toEqual(false);
    expect(mockNormalize).not.toHaveBeenCalled();

    const args2: IParsedAdaptation = {
      id: "12",
      representations: [],
      closedCaption: true,
      type: "video",
    };
    const adaptation2 = new Adaptation(args2, codecSupportCache);
    expect(adaptation2.language).toBe(undefined);
    expect(adaptation2.normalizedLanguage).toBe(undefined);
    expect(adaptation2.isClosedCaption).toEqual(true);
    expect(mockNormalize).not.toHaveBeenCalled();
  });

  it("should set an isAudioDescription value if one", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));
    const mockNormalize = vi.fn((lang: string) => lang + "foo");

    vi.doMock("../../../utils/languages", () => ({
      default: mockNormalize,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;

    const args1: IParsedAdaptation = {
      id: "12",
      representations: [],
      audioDescription: false,
      type: "video",
    };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation1 = new Adaptation(args1, codecSupportCache);
    expect(adaptation1.language).toBe(undefined);
    expect(adaptation1.normalizedLanguage).toBe(undefined);
    expect(adaptation1.isAudioDescription).toEqual(false);
    expect(mockNormalize).not.toHaveBeenCalled();

    const args2: IParsedAdaptation = {
      id: "12",
      representations: [],
      audioDescription: true,
      type: "video",
    };
    const adaptation2 = new Adaptation(args2, codecSupportCache);
    expect(adaptation2.language).toBe(undefined);
    expect(adaptation2.normalizedLanguage).toBe(undefined);
    expect(adaptation2.isAudioDescription).toEqual(true);
    expect(mockNormalize).not.toHaveBeenCalled();
  });

  it("should set a manuallyAdded value if one", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));
    const mockNormalize = vi.fn((lang: string) => lang + "foo");
    vi.doMock("../../../utils/languages", () => ({
      default: mockNormalize,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;

    const args1: IParsedAdaptation = { id: "12", representations: [], type: "video" };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation1 = new Adaptation(args1, codecSupportCache, {
      isManuallyAdded: false,
    });
    expect(adaptation1.language).toBe(undefined);
    expect(adaptation1.normalizedLanguage).toBe(undefined);
    expect(adaptation1.manuallyAdded).toEqual(false);
    expect(mockNormalize).not.toHaveBeenCalled();

    const args2: IParsedAdaptation = { id: "12", representations: [], type: "video" };
    const adaptation2 = new Adaptation(args2, codecSupportCache, {
      isManuallyAdded: true,
    });
    expect(adaptation2.language).toBe(undefined);
    expect(adaptation2.normalizedLanguage).toBe(undefined);
    expect(adaptation2.manuallyAdded).toEqual(true);
    expect(mockNormalize).not.toHaveBeenCalled();
  });

  it("should return the first Representation with the given Id with `getRepresentation`", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;
    const rep1 = {
      bitrate: 10,
      id: "rep1",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep2 = {
      bitrate: 20,
      id: "rep2",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep3 = {
      bitrate: 30,
      id: "rep2",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const representations = [rep1, rep2, rep3];
    const args: IParsedAdaptation = { id: "12", representations, type: "text" as const };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation = new Adaptation(args, codecSupportCache);

    expect(adaptation.getRepresentation("rep1")?.bitrate).toEqual(10);
    expect(adaptation.getRepresentation("rep2")?.bitrate).toEqual(20);
  });

  it("should return undefined in `getRepresentation` if no representation is found with this Id", async () => {
    vi.doMock("../representation", () => ({
      default: mockDefaultRepresentationImpl,
    }));

    const Adaptation = (await vi.importActual("../adaptation"))
      .default as typeof IAdaptation;
    const rep1 = {
      bitrate: 10,
      id: "rep1",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep2 = {
      bitrate: 20,
      id: "rep2",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const rep3 = {
      bitrate: 30,
      id: "rep2",
      cdnMetadata: [],
      index: minimalRepresentationIndex,
    };
    const representations = [rep1, rep2, rep3];
    const args: IParsedAdaptation = { id: "12", representations, type: "text" as const };
    const codecSupportCache = new CodecSupportCache([]);
    const adaptation = new Adaptation(args, codecSupportCache);

    expect(adaptation.getRepresentation("rep5")).toBe(undefined);
  });
});
