import { describe, it, expect } from "vitest";
import type { MediaItem } from "$lib/api/types";
import { isCurrentEpisode, adjacentEpisodes, compareSeriesOrder, stripCardLabel } from "./episodeStrip";

// Minimal episode factory — only the fields the strip logic reads.
function ep(
  id: string,
  season: number | null,
  number: number | null,
): MediaItem {
  return {
    id,
    name: `S${season}E${number}`,
    kind: "episode",
    parentIndexNumber: season,
    indexNumber: number,
  } as unknown as MediaItem;
}

function season(n: number, count: number): MediaItem[] {
  return Array.from({ length: count }, (_, i) => ep(`s${n}e${i + 1}`, n, i + 1));
}

describe("isCurrentEpisode", () => {
  const current = ep("abc", 1, 3);

  it("matches by id", () => {
    expect(isCurrentEpisode(ep("abc", 9, 9), current)).toBe(true);
  });

  it("matches by season+episode number when id differs", () => {
    expect(isCurrentEpisode(ep("other", 1, 3), current)).toBe(true);
  });

  it("does not match a different episode number", () => {
    expect(isCurrentEpisode(ep("other", 1, 4), current)).toBe(false);
  });

  it("does NOT treat two number-less episodes as the same (the reported bug)", () => {
    const a = ep("a", null, null);
    const b = ep("b", null, null);
    expect(isCurrentEpisode(a, b)).toBe(false);
  });

  it("does not match when only one side has numbers", () => {
    expect(isCurrentEpisode(ep("a", null, null), current)).toBe(false);
    expect(isCurrentEpisode(ep("a", 1, 3), ep("b", null, null))).toBe(false);
  });
});

describe("adjacentEpisodes", () => {
  it("returns just the current episode when there are no others", () => {
    const current = ep("only", 1, 1);
    expect(adjacentEpisodes(current, [])).toEqual([current]);
  });

  it("returns siblings, not just the current episode", () => {
    const eps = season(1, 8);
    const current = eps[2]; // S1E3
    const strip = adjacentEpisodes(current, eps);
    expect(strip.length).toBeGreaterThan(1);
    expect(strip).toContain(current);
  });

  it("windows to 3 before and 6 after the current episode", () => {
    const eps = season(1, 20);
    const current = eps[9]; // S1E10, index 9
    const strip = adjacentEpisodes(current, eps);
    // start = max(0, 9-3)=6 (E7), end = min(20, 9+7)=16 → E7..E16 (10 items)
    expect(strip.map((e) => e.indexNumber)).toEqual([7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
    expect(strip).toContain(current);
  });

  // ux-flows §5B.2, "Cross-season continuity": the window spans the whole
  // series in episode order, so it runs past a season boundary rather than
  // dead-ending at the end of a season.
  it("runs past the end of a season into the next one", () => {
    const eps = [...season(1, 5), ...season(2, 5)];
    const current = eps[4]; // S1E5 — the season finale
    const strip = adjacentEpisodes(current, eps);
    expect(strip.map((e) => e.id)).toEqual([
      "s1e2", "s1e3", "s1e4", "s1e5",
      "s2e1", "s2e2", "s2e3", "s2e4", "s2e5",
    ]);
  });

  it("reaches back into the previous season from a season opener", () => {
    const eps = [...season(1, 5), ...season(2, 5)];
    const current = eps[5]; // S2E1
    const strip = adjacentEpisodes(current, eps);
    expect(strip.slice(0, 3).map((e) => e.id)).toEqual(["s1e3", "s1e4", "s1e5"]);
    expect(strip[3].id).toBe("s2e1");
  });

  it("orders by season then episode, never interleaving seasons", () => {
    const eps = [...season(2, 3), ...season(1, 3)]; // deliberately out of order
    const current = eps[3]; // S1E1
    const strip = adjacentEpisodes(current, eps);
    expect(strip.map((e) => e.id)).toEqual([
      "s1e1", "s1e2", "s1e3", "s2e1", "s2e2", "s2e3",
    ]);
  });

  it("sorts specials (season 0) after the numbered seasons", () => {
    const eps = [...season(0, 2), ...season(1, 2)];
    const current = eps[2]; // S1E1
    const strip = adjacentEpisodes(current, eps);
    expect(strip.map((e) => e.id)).toEqual(["s1e1", "s1e2", "s0e1", "s0e2"]);
  });

  it("splices in a directly-fetched episode absent from the list (ID mismatch)", () => {
    const eps = season(1, 5);
    // Focused episode has a different id than any in the list but same numbers.
    const current = ep("fetched-directly", 1, 3);
    const strip = adjacentEpisodes(current, eps);
    // It should appear once, anchored at its numeric position, alongside siblings.
    expect(strip.filter((e) => e.indexNumber === 3).length).toBe(1);
    expect(strip.length).toBeGreaterThan(1);
  });

  it("falls back to the full list when the current season is unknown", () => {
    const eps = season(1, 5);
    const current = ep("mystery", null, 3); // no season number
    const strip = adjacentEpisodes(current, eps);
    expect(strip.length).toBeGreaterThan(1);
    // Anchored at its episode number, not dumped at one end of the list.
    expect(strip.indexOf(current)).toBeGreaterThan(0);
    expect(strip.indexOf(current)).toBeLessThan(strip.length - 1);
  });
});

describe("compareSeriesOrder", () => {
  it("orders by season, then episode", () => {
    expect(compareSeriesOrder(ep("a", 1, 9), ep("b", 2, 1))).toBeLessThan(0);
    expect(compareSeriesOrder(ep("a", 2, 1), ep("b", 2, 2))).toBeLessThan(0);
    expect(compareSeriesOrder(ep("a", 2, 2), ep("b", 2, 2))).toBe(0);
  });

  it("puts specials last", () => {
    expect(compareSeriesOrder(ep("a", 0, 1), ep("b", 1, 1))).toBeGreaterThan(0);
    expect(compareSeriesOrder(ep("a", 0, 1), ep("b", 9, 1))).toBeGreaterThan(0);
  });

  it("falls back to episode number when a season is unknown", () => {
    expect(compareSeriesOrder(ep("a", null, 2), ep("b", 1, 5))).toBeLessThan(0);
  });
});

describe("stripCardLabel", () => {
  const current = ep("cur", 2, 4);

  it("shows a bare episode number within the current season", () => {
    expect(stripCardLabel(ep("a", 2, 6), current)).toBe("6.");
  });

  it("shows SxEy once the card crosses a season boundary", () => {
    expect(stripCardLabel(ep("a", 3, 1), current)).toBe("S3E1");
    expect(stripCardLabel(ep("a", 1, 8), current)).toBe("S1E8");
  });

  it("degrades to the episode number when the season is unknown", () => {
    expect(stripCardLabel(ep("a", null, 7), current)).toBe("7.");
  });
});
