import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { concat } from "../../../../utils/byte_parsing";
import extractPssh, { getPsshSystemID } from "../extract_pssh";

const { mockCanPatchOutPsshBox, mockLog } = vi.hoisted(() => {
  return {
    mockCanPatchOutPsshBox: vi.fn(() => false),
    mockLog: {
      warn: vi.fn(),
      info: vi.fn(),
      debug: vi.fn(),
      log: vi.fn(),
      error: vi.fn(),
    },
  };
});

vi.mock("../../../../compat/can_patch_out_pssh", () => ({
  default: mockCanPatchOutPsshBox,
}));

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

/** Build a minimal moov box wrapping `innerBytes`. */
function buildMoovBox(innerBytes: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {
  const size = 8 + innerBytes.length;
  const buf = new Uint8Array(size);
  const view = new DataView(buf.buffer);
  view.setUint32(0, size, false);
  buf[4] = 0x6d;
  buf[5] = 0x6f;
  buf[6] = 0x6f;
  buf[7] = 0x76; // "moov"
  buf.set(innerBytes, 8);
  return buf;
}

/**
 * Build a pssh box.
 *
 * Layout (after size + name):
 *   1 byte  version
 *   3 bytes flags
 *  16 bytes systemId
 *   N bytes extra data
 */
function buildPsshBox(
  systemId: Uint8Array<ArrayBuffer>,
  version = 0,
  extraData: Uint8Array<ArrayBuffer> = new Uint8Array(0),
): Uint8Array<ArrayBuffer> {
  // 8 (size+name) + 1 (version) + 3 (flags) + 16 (systemId) + extraData
  const size = 8 + 1 + 3 + 16 + extraData.length;
  const buf = new Uint8Array(size);
  const view = new DataView(buf.buffer);
  view.setUint32(0, size, false);
  buf[4] = 0x70;
  buf[5] = 0x73;
  buf[6] = 0x73;
  buf[7] = 0x68; // "pssh"
  buf[8] = version; // version (bytes 0 of version+flags)
  // flags stay 0x000000
  buf.set(systemId, 12); // offset 8+4 = 12
  buf.set(extraData, 28);
  return buf;
}

const SYSTEM_ID_A = new Uint8Array([
  0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2,
  0xfb, 0x4b,
]);
const SYSTEM_ID_B = new Uint8Array([
  0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d,
  0x21, 0xed,
]);

const systemIdAHex = Array.from(SYSTEM_ID_A)
  .map((b) => b.toString(16).padStart(2, "0"))
  .join("");
const systemIdBHex = Array.from(SYSTEM_ID_B)
  .map((b) => b.toString(16).padStart(2, "0"))
  .join("");

describe("extractPssh", () => {
  beforeEach(() => {
    mockCanPatchOutPsshBox.mockReturnValue(true);
  });
  afterEach(() => {
    vi.resetAllMocks();
  });

  it("returns [] when there is no moov box", () => {
    const data = new Uint8Array(32); // no moov, only `0`
    expect(extractPssh(data)).toEqual([]);
  });

  it("returns [] when moov contains no pssh box", () => {
    // An empty moov (just the 8-byte header, no inner boxes)
    const data = buildMoovBox(new Uint8Array(0));
    expect(extractPssh(data)).toEqual([]);
  });

  it("extracts a single pssh box", () => {
    const pssh = buildPsshBox(SYSTEM_ID_A);
    const data = buildMoovBox(pssh);
    const result = extractPssh(data);
    expect(result).toHaveLength(1);
    expect(result[0].systemId).toBe(systemIdAHex);
  });

  it("extracts multiple pssh boxes in order", () => {
    const psshA = buildPsshBox(SYSTEM_ID_A);
    const psshB = buildPsshBox(SYSTEM_ID_B);
    const data = buildMoovBox(concat(psshA, psshB));
    const result = extractPssh(data);
    expect(result).toHaveLength(2);
    expect(result[0].systemId).toBe(systemIdAHex);
    expect(result[1].systemId).toBe(systemIdBHex);
  });

  it("skips a pssh whose systemId cannot be parsed and keeps going", () => {
    // A pssh with version > 1 triggers the 'un-handled PSSH version' warn path,
    // so getPsshSystemID returns undefined and the box is omitted.
    const psshBad = buildPsshBox(SYSTEM_ID_A, 2 /* unsupported version */);
    const psshGood = buildPsshBox(SYSTEM_ID_B);
    const data = buildMoovBox(concat(psshBad, psshGood));
    const result = extractPssh(data);
    expect(result).toHaveLength(1);
    expect(result[0].systemId).toBe(systemIdBHex);
    expect(mockLog.warn).toHaveBeenCalledWith("isobmff", "un-handled PSSH version");
  });

  it("patches out pssh bytes when canPatchOutPsshBox returns true", () => {
    mockCanPatchOutPsshBox.mockReturnValue(true);
    const pssh = buildPsshBox(SYSTEM_ID_A);
    const moov = buildMoovBox(pssh);
    // The pssh box starts at offset 8 inside moov content (right after moov header).
    // After patching, bytes 4–7 of the pssh box should be "free" (0x66 0x72 0x65 0x65).
    extractPssh(moov);
    // moov content starts at offset 8 of the full segment.
    // pssh name is at moov_content[4..7] = segment[12..15]
    expect(moov[12]).toBe(0x66); // 'f'
    expect(moov[13]).toBe(0x72); // 'r'
    expect(moov[14]).toBe(0x65); // 'e'
    expect(moov[15]).toBe(0x65); // 'e'
  });

  it("does not patch bytes when canPatchOutPsshBox returns false", () => {
    mockCanPatchOutPsshBox.mockReturnValue(false);
    const pssh = buildPsshBox(SYSTEM_ID_A);
    const originalNameByte1 = pssh[4]; // should be 0x70 ('p')
    const originalNameByte2 = pssh[5]; // 's'
    const originalNameByte3 = pssh[6]; // 's'
    const originalNameByte4 = pssh[7]; // 'h'
    const moov = buildMoovBox(pssh);
    extractPssh(moov);
    expect(moov[12]).toBe(originalNameByte1);
    expect(moov[13]).toBe(originalNameByte2);
    expect(moov[14]).toBe(originalNameByte3);
    expect(moov[15]).toBe(originalNameByte4);
  });
});

describe("getPsshSystemID", () => {
  afterEach(() => {
    vi.resetAllMocks();
  });

  it("parses a valid version-0 pssh and returns hex systemId", () => {
    const pssh = buildPsshBox(SYSTEM_ID_A);
    // initialDataOffset is the offset right after size (4) + name (4) = 8
    const result = getPsshSystemID(pssh, 8);
    expect(result).toBe(systemIdAHex);
  });

  it("parses a valid version-1 pssh", () => {
    const pssh = buildPsshBox(SYSTEM_ID_B, 1);
    const result = getPsshSystemID(pssh, 8);
    expect(result).toBe(systemIdBHex);
  });

  it("returns undefined and warns for version > 1", () => {
    const pssh = buildPsshBox(SYSTEM_ID_A, 2);
    const result = getPsshSystemID(pssh, 8);
    expect(result).toBeUndefined();
    expect(mockLog.warn).toHaveBeenCalledWith("isobmff", "un-handled PSSH version");
  });

  it("returns undefined when buffer is too short to contain a full systemId", () => {
    // Build a buffer that's big enough for the header (version + flags = 4 bytes)
    // but lacks the 16-byte systemId.
    const buf = new Uint8Array(8 + 4 + 10); // only 10 bytes where 16 are needed
    const result = getPsshSystemID(buf, 8);
    expect(result).toBeUndefined();
    expect(mockLog.warn).not.toHaveBeenCalled();
  });
});
