import { describe, beforeEach, afterEach, it, expect, vi } from "vitest";
import type { IManifestStreamEvent } from "../../../parsers/manifest";
import type { IPeriod, IPeriodMetadata } from "../../index";
import type { IManifestAdaptations, IThumbnailTrack } from "../period";
import { replacePeriods, updatePeriods } from "../update_periods";

const mocks = vi.hoisted(() => {
  return {
    updatePeriodInPlace: vi.fn(),
  };
});
vi.mock("../update_period_in_place.ts", () => ({
  default: mocks.updatePeriodInPlace,
}));

const MANIFEST_UPDATE_TYPE = {
  Full: 0,
  Partial: 1,
};

const fakeUpdatePeriodInPlaceRes = {
  updatedAdaptations: [],
  removedAdaptations: [],
  addedAdaptations: [],
};

class FakePeriod implements IPeriodMetadata {
  public readonly id: string;
  public adaptations: IManifestAdaptations;
  public start: number;
  public duration: number | undefined;
  public end: number | undefined;
  public streamEvents: IManifestStreamEvent[];
  public thumbnailTracks: IThumbnailTrack[];

  constructor({
    id,
    start,
    end,
  }: {
    id: string;
    start?: number | undefined;
    end?: number | undefined;
  }) {
    this.id = id ?? String(start);
    this.start = start ?? 0;
    this.end = end;
    this.duration = end === undefined ? undefined : end - (start ?? 0);
    this.streamEvents = [];
    this.adaptations = {};
    this.thumbnailTracks = [];
  }
  createAdaptationsObject() {
    return {};
  }
  getMediaSupport() {
    return {
      video: true,
      audio: true,
      text: true,
    };
  }

  refreshCodecSupport() {
    // noop
  }
  getAdaptations() {
    return [];
  }
  getAdaptationsForType() {
    return [];
  }
  getAdaptation(): undefined {
    return undefined;
  }
  getSupportedAdaptations() {
    return [];
  }
  containsTime() {
    return false;
  }
  getMetadataSnapshot() {
    return {
      start: this.start ?? 0,
      end: this.end,
      id: this.id ?? String(this.start),
      streamEvents: [],
      adaptations: {},
      thumbnailTracks: [],
    };
  }
}

function generateFakePeriod({
  id,
  start,
  end,
}: {
  id: string;
  start?: number | undefined;
  end?: number | undefined;
}): IPeriod {
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
  return new FakePeriod({ id, start, end }) as IPeriod;
}

describe("Manifest - replacePeriods", () => {
  beforeEach(() => {
    vi.resetModules();
  });
  afterEach(() => {
    mocks.updatePeriodInPlace.mockReset();
  });

  // Case 1 :
  //
  // old periods : p1, p2
  // new periods : p2
  it("should remove old period", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p1", start: 0 }),
      generateFakePeriod({ id: "p2" }),
    ];
    const initialPeriods = oldPeriods.slice();
    const newPeriods = [generateFakePeriod({ id: "p2" })];
    const res = replacePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [],
      removedPeriods: [{ start: 0, id: "p1", end: undefined }],
      updatedPeriods: [
        {
          period: {
            id: initialPeriods[1].id,
            start: initialPeriods[1].start,
            end: initialPeriods[1].end,
            duration: initialPeriods[1].duration,
            streamEvents: initialPeriods[1].streamEvents,
          },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(1);
    expect(oldPeriods[0].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(1);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[1],
      newPeriods[0],
      0,
    );
  });

  // Case 2 :
  //
  // old periods : p1
  // new periods : p1, p2
  it("should add new period", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p2" })];
    const initialPeriods = oldPeriods.slice();
    const newPeriods = [
      generateFakePeriod({ id: "p2" }),
      generateFakePeriod({ id: "p3" }),
    ];
    const res = replacePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [newPeriods[1].getMetadataSnapshot()],
      removedPeriods: [],
      updatedPeriods: [
        {
          period: {
            id: initialPeriods[0].id,
            start: initialPeriods[0].start,
            end: initialPeriods[0].end,
            duration: initialPeriods[0].duration,
            streamEvents: initialPeriods[0].streamEvents,
          },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p2");
    expect(oldPeriods[1].id).toBe("p3");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(1);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[0],
      newPeriods[0],
      0,
    );
  });

  // Case 3 :
  //
  // old periods: p1
  // new periods: p2
  it("should replace period", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p1" })];
    const newPeriods = [generateFakePeriod({ id: "p2" })];
    const res = replacePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [newPeriods[0].getMetadataSnapshot()],
      removedPeriods: [{ id: "p1", start: 0 }],
      updatedPeriods: [],
    });
    expect(oldPeriods.length).toBe(1);
    expect(oldPeriods[0].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 4 :
  //
  // old periods: p0, p1, p2
  // new periods: p1, a, b, p2, p3
  it("should handle more complex period replacement", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p0" }),
      generateFakePeriod({ id: "p1" }),
      generateFakePeriod({ id: "p2", start: 0 }),
    ];
    const newPeriods = [
      generateFakePeriod({ id: "p1" }),
      generateFakePeriod({ id: "a" }),
      generateFakePeriod({ id: "b" }),
      generateFakePeriod({ id: "p2", start: 2 }),
      generateFakePeriod({ id: "p3" }),
    ];
    const initialPeriods = oldPeriods.slice();
    const res = replacePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [
        newPeriods[1].getMetadataSnapshot(),
        newPeriods[2].getMetadataSnapshot(),
        newPeriods[4].getMetadataSnapshot(),
      ],
      removedPeriods: [{ id: "p0", start: 0 }],
      updatedPeriods: [
        {
          period: { id: "p1", start: 0, streamEvents: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
        {
          period: { id: "p2", start: 0, streamEvents: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(5);

    expect(oldPeriods[0].id).toBe("p1");
    expect(oldPeriods[1].id).toBe("a");
    expect(oldPeriods[2].id).toBe("b");
    expect(oldPeriods[3].id).toBe("p2");
    expect(oldPeriods[4].id).toBe("p3");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(2);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[1],
      newPeriods[0],
      0,
    );
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      2,
      initialPeriods[2],
      newPeriods[3],
      0,
    );
  });

  // Case 5 :
  //
  // old periods : p2
  // new periods : p1, p2
  it("should add new period before", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p2" })];
    const newPeriods = [
      generateFakePeriod({ id: "p1" }),
      generateFakePeriod({ id: "p2" }),
    ];
    const initialPeriods = oldPeriods.slice();
    const res = replacePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [newPeriods[0].getMetadataSnapshot()],
      removedPeriods: [],
      updatedPeriods: [
        {
          period: { id: "p2", start: 0, streamEvents: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p1");
    expect(oldPeriods[1].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(1);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[0],
      newPeriods[1],
      MANIFEST_UPDATE_TYPE.Full,
    );
  });

  // Case 6 :
  //
  // old periods : p1, p2
  // new periods : No periods
  it("should remove all periods", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p1", start: 0 }),
      generateFakePeriod({ id: "p2", start: 0 }),
    ];
    const newPeriods: IPeriod[] = [];
    const res = replacePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [],
      removedPeriods: [
        { id: "p1", start: 0 },
        { id: "p2", start: 0 },
      ],
      updatedPeriods: [],
    });
    expect(oldPeriods.length).toBe(0);
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 7 :
  //
  // old periods : No periods
  // new periods : p1, p2
  it("should add all periods to empty array", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods: IPeriod[] = [];
    const newPeriods = [
      generateFakePeriod({ id: "p1" }),
      generateFakePeriod({ id: "p2" }),
    ];
    const res = replacePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [
        newPeriods[0].getMetadataSnapshot(),
        newPeriods[1].getMetadataSnapshot(),
      ],
      removedPeriods: [],
      updatedPeriods: [],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p1");
    expect(oldPeriods[1].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });
});

describe("Manifest - updatePeriods", () => {
  beforeEach(() => {
    vi.resetModules();
  });
  afterEach(() => {
    mocks.updatePeriodInPlace.mockReset();
  });

  // Case 1 :
  //
  // old periods : p1, p2
  // new periods : p2
  it("should not remove old period", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p1", start: 50, end: 60 }),
      generateFakePeriod({ id: "p2", start: 60 }),
    ];
    const newPeriods = [generateFakePeriod({ id: "p2", start: 60 })];
    const initialPeriods = oldPeriods.slice();

    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [],
      removedPeriods: [],
      updatedPeriods: [
        {
          period: { id: "p2", start: 60, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p1");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(1);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[1],
      newPeriods[0],
      MANIFEST_UPDATE_TYPE.Partial,
    );
  });

  // Case 2 :
  //
  // old periods : p1
  // new periods : p1, p2
  it("should add new period", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p2", start: 60 })];
    const newPeriods = [
      generateFakePeriod({ id: "p2", start: 60, end: 80 }),
      generateFakePeriod({ id: "p3", start: 80 }),
    ];
    const initialPeriods = oldPeriods.slice();
    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [newPeriods[1].getMetadataSnapshot()],
      removedPeriods: [],
      updatedPeriods: [
        {
          period: { id: "p2", start: 60, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p2");
    expect(oldPeriods[1].id).toBe("p3");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(1);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[0],
      newPeriods[0],
      MANIFEST_UPDATE_TYPE.Partial,
    );
  });

  // Case 3 :
  //
  // old periods: p1
  // new periods: p3
  it("should throw when encountering two distant Periods", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p1", start: 50, end: 60 })];
    const newPeriods = [generateFakePeriod({ id: "p3", start: 70, end: 80 })];

    let error: unknown = null;
    try {
      updatePeriods(oldPeriods, newPeriods);
    } catch (e) {
      error = e;
    }

    expect(error).toBeInstanceOf(Error);

    // Impossible check to shut-up TypeScript
    if (!(error instanceof Error)) {
      throw new Error("Impossible: already checked it was an Error instance");
    }
    expect((error as { type?: string }).type).toEqual("MEDIA_ERROR");
    expect((error as { code?: string }).code).toEqual("MANIFEST_UPDATE_ERROR");
    expect(error.message).toEqual(
      "MANIFEST_UPDATE_ERROR: Cannot perform partial update: not enough data",
    );
    expect(oldPeriods.length).toBe(1);
    expect(oldPeriods[0].id).toBe("p1");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 4 :
  //
  // old periods: p0, p1, p2
  // new periods: p1, a, b, p2, p3
  it("should handle more complex period replacement", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p0", start: 50, end: 60 }),
      generateFakePeriod({ id: "p1", start: 60, end: 69 }),
      generateFakePeriod({ id: "p1.5", start: 69, end: 70 }),
      generateFakePeriod({ id: "p2", start: 70 }),
    ];
    const newPeriods = [
      generateFakePeriod({ id: "p1", start: 60, end: 65 }),
      generateFakePeriod({ id: "a", start: 65, end: 68 }),
      generateFakePeriod({ id: "b", start: 68, end: 70 }),
      generateFakePeriod({ id: "p2", start: 70, end: 80 }),
      generateFakePeriod({ id: "p3", start: 80 }),
    ];
    const initialPeriods = oldPeriods.slice();
    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [
        newPeriods[1].getMetadataSnapshot(),
        newPeriods[2].getMetadataSnapshot(),
        newPeriods[4].getMetadataSnapshot(),
      ],
      removedPeriods: [{ id: "p1.5", start: 69, end: 70 }],
      updatedPeriods: [
        {
          period: { id: "p1", start: 60, end: 69, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
        {
          period: { id: "p2", start: 70, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(6);

    expect(oldPeriods[0].id).toBe("p0");
    expect(oldPeriods[1].id).toBe("p1");
    expect(oldPeriods[2].id).toBe("a");
    expect(oldPeriods[3].id).toBe("b");
    expect(oldPeriods[4].id).toBe("p2");
    expect(oldPeriods[5].id).toBe("p3");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(2);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[1],
      newPeriods[0],
      MANIFEST_UPDATE_TYPE.Partial,
    );
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      2,
      initialPeriods[3],
      newPeriods[3],
      MANIFEST_UPDATE_TYPE.Full,
    );
  });

  // Case 5 :
  //
  // old periods : p2
  // new periods : p1, p2
  it("should throw when the first period is not encountered", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p2", start: 70 })];
    const newPeriods = [
      generateFakePeriod({ id: "p1", start: 50, end: 70 }),
      generateFakePeriod({ id: "p2", start: 70 }),
    ];

    let error: unknown = null;
    try {
      updatePeriods(oldPeriods, newPeriods);
    } catch (e) {
      error = e;
    }

    expect(error).toBeInstanceOf(Error);

    // Impossible check to shut-up TypeScript
    if (!(error instanceof Error)) {
      throw new Error("Impossible: already checked it was an Error instance");
    }
    expect((error as { type?: string }).type).toEqual("MEDIA_ERROR");
    expect((error as { code?: string }).code).toEqual("MANIFEST_UPDATE_ERROR");
    expect(error.message).toEqual(
      "MANIFEST_UPDATE_ERROR: Cannot perform partial update: incoherent data",
    );
    expect(oldPeriods.length).toBe(1);
    expect(oldPeriods[0].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 6 :
  //
  // old periods : p1, p2
  // new periods : No periods
  it("should keep old periods if no new Period is available", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p1" }),
      generateFakePeriod({ id: "p2" }),
    ];
    const newPeriods: IPeriod[] = [];
    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [],
      removedPeriods: [],
      updatedPeriods: [],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p1");
    expect(oldPeriods[1].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 7 :
  //
  // old periods : No periods
  // new periods : p1, p2
  it("should set only new Periods if none were available before", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods: IPeriod[] = [];
    const newPeriods = [
      generateFakePeriod({ id: "p1" }),
      generateFakePeriod({ id: "p2" }),
    ];
    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [
        newPeriods[0].getMetadataSnapshot(),
        newPeriods[1].getMetadataSnapshot(),
      ],
      removedPeriods: [],
      updatedPeriods: [],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p1");
    expect(oldPeriods[1].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 8 :
  //
  // old periods : p0, p1
  // new periods : p4, p5
  it("should throw if the new periods come strictly after", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p0", start: 50, end: 60 }),
      generateFakePeriod({ id: "p1", start: 60, end: 70 }),
    ];
    const newPeriods = [generateFakePeriod({ id: "p3", start: 80 })];

    let error: unknown = null;
    try {
      updatePeriods(oldPeriods, newPeriods);
    } catch (e) {
      error = e;
    }

    expect(error).toBeInstanceOf(Error);

    // Impossible check to shut-up TypeScript
    if (!(error instanceof Error)) {
      throw new Error("Impossible: already checked it was an Error instance");
    }
    expect((error as { type?: string }).type).toEqual("MEDIA_ERROR");
    expect((error as { code?: string }).code).toEqual("MANIFEST_UPDATE_ERROR");
    expect(error.message).toEqual(
      "MANIFEST_UPDATE_ERROR: Cannot perform partial update: not enough data",
    );
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p0");
    expect(oldPeriods[1].id).toBe("p1");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 9 :
  //
  // old periods: p1
  // new periods: p2
  it("should concatenate consecutive periods", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p1", start: 50, end: 60 })];
    const newPeriods = [generateFakePeriod({ id: "p2", start: 60, end: 80 })];

    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [newPeriods[0].getMetadataSnapshot()],
      removedPeriods: [],
      updatedPeriods: [],
    });
    expect(oldPeriods.length).toBe(2);
    expect(oldPeriods[0].id).toBe("p1");
    expect(oldPeriods[1].id).toBe("p2");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 10 :
  //
  // old periods: p1
  // new periods: px
  it("should throw when encountering two completely different Periods with the same start", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [generateFakePeriod({ id: "p1", start: 50, end: 60 })];
    const newPeriods = [generateFakePeriod({ id: "px", start: 50, end: 70 })];

    let error: unknown = null;
    try {
      updatePeriods(oldPeriods, newPeriods);
    } catch (e) {
      error = e;
    }

    expect(error).toBeInstanceOf(Error);

    // Impossible check to shut-up TypeScript
    if (!(error instanceof Error)) {
      throw new Error("Impossible: already checked it was an Error instance");
    }
    expect((error as { type?: string }).type).toEqual("MEDIA_ERROR");
    expect((error as { code?: string }).code).toEqual("MANIFEST_UPDATE_ERROR");
    expect(error.message).toEqual(
      "MANIFEST_UPDATE_ERROR: Cannot perform partial update: incoherent data",
    );
    expect(oldPeriods.length).toBe(1);
    expect(oldPeriods[0].id).toBe("p1");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(0);
  });

  // Case 11 :
  //
  // old periods: p0, p1, p2
  // new periods: p1, p2, p3
  it("should handle more complex period replacement", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p0", start: 50, end: 60 }),
      generateFakePeriod({ id: "p1", start: 60, end: 70 }),
      generateFakePeriod({ id: "p2", start: 70 }),
    ];
    const newPeriods = [
      generateFakePeriod({ id: "p1", start: 60, end: 65 }),
      generateFakePeriod({ id: "p2", start: 65, end: 80 }),
      generateFakePeriod({ id: "p3", start: 80 }),
    ];
    const initialPeriods = oldPeriods.slice();
    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [newPeriods[2].getMetadataSnapshot()],
      removedPeriods: [],
      updatedPeriods: [
        {
          period: { id: "p1", start: 60, end: 70, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
        {
          period: { id: "p2", start: 70, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(4);

    expect(oldPeriods[0].id).toBe("p0");
    expect(oldPeriods[1].id).toBe("p1");
    expect(oldPeriods[2].id).toBe("p2");
    expect(oldPeriods[3].id).toBe("p3");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(2);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[1],
      newPeriods[0],
      MANIFEST_UPDATE_TYPE.Partial,
    );
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      2,
      initialPeriods[2],
      newPeriods[1],
      MANIFEST_UPDATE_TYPE.Full,
    );
  });

  // Case 12 :
  //
  // old periods: p0, p1, p2, p3
  // new periods: p1, p3
  it("should handle more complex period replacement", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p0", start: 50, end: 60 }),
      generateFakePeriod({ id: "p1", start: 60, end: 70 }),
      generateFakePeriod({ id: "p2", start: 70, end: 80 }),
      generateFakePeriod({ id: "p3", start: 80 }),
    ];
    const newPeriods = [
      generateFakePeriod({ id: "p1", start: 60, end: 70 }),
      generateFakePeriod({ id: "p3", start: 80 }),
    ];
    const initialPeriods = oldPeriods.slice();
    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [],
      removedPeriods: [{ id: "p2", start: 70, end: 80 }],
      updatedPeriods: [
        {
          period: { id: "p1", start: 60, end: 70, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
        {
          period: { id: "p3", start: 80, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(3);

    expect(oldPeriods[0].id).toBe("p0");
    expect(oldPeriods[1].id).toBe("p1");
    expect(oldPeriods[2].id).toBe("p3");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(2);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[1],
      newPeriods[0],
      MANIFEST_UPDATE_TYPE.Partial,
    );
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      2,
      initialPeriods[3],
      newPeriods[1],
      MANIFEST_UPDATE_TYPE.Full,
    );
  });

  // Case 13 :
  //
  // old periods: p0, p1, p2, p3, p4
  // new periods: p1, p3
  it("should remove periods not included in the new Periods", () => {
    mocks.updatePeriodInPlace.mockImplementation(() => fakeUpdatePeriodInPlaceRes);
    const oldPeriods = [
      generateFakePeriod({ id: "p0", start: 50, end: 60 }),
      generateFakePeriod({ id: "p1", start: 60, end: 70 }),
      generateFakePeriod({ id: "p2", start: 70, end: 80 }),
      generateFakePeriod({ id: "p3", start: 80, end: 90 }),
      generateFakePeriod({ id: "p4", start: 90 }),
    ];
    const initialPeriods = oldPeriods.slice();
    const newPeriods = [
      generateFakePeriod({ id: "p1", start: 60, end: 70 }),
      generateFakePeriod({ id: "p3", start: 80, end: 90 }),
    ];
    const res = updatePeriods(oldPeriods, newPeriods);
    expect(res).toEqual({
      addedPeriods: [],
      removedPeriods: [
        { id: "p2", start: 70, end: 80 },
        { id: "p4", start: 90 },
      ],
      updatedPeriods: [
        {
          period: { id: "p1", start: 60, end: 70, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
        {
          period: { id: "p3", start: 80, end: 90, streamEvents: [], thumbnailTracks: [] },
          result: fakeUpdatePeriodInPlaceRes,
        },
      ],
    });
    expect(oldPeriods.length).toBe(3);

    expect(oldPeriods[0].id).toBe("p0");
    expect(oldPeriods[1].id).toBe("p1");
    expect(oldPeriods[2].id).toBe("p3");
    expect(mocks.updatePeriodInPlace).toHaveBeenCalledTimes(2);
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      1,
      initialPeriods[1],
      newPeriods[0],
      MANIFEST_UPDATE_TYPE.Partial,
    );
    expect(mocks.updatePeriodInPlace).toHaveBeenNthCalledWith(
      2,
      initialPeriods[3],
      newPeriods[1],
      MANIFEST_UPDATE_TYPE.Full,
    );
  });
});
