import { describe, expect, it } from "vitest";
import {
  ROLL_EPSILON,
  groupStrips,
  pickStripAnchor,
  pickStripEnd,
  type StripGrouping,
} from "./slotStrips";

/** Минимальный слот для группировки. tn задаём как локальную ISO-строку (как отдаёт бэкенд). */
type S = {
  satelliteId: number;
  slotNumber: number;
  cycle: number;
  tn: string;
  tk: string;
  roll: number;
};

const keyOf = (s: S) => `${s.satelliteId}:${s.slotNumber}:${s.cycle}`;

/** Слот на минуту mm (от базовой даты) с длительностью 30 c. cycle ≈ «виток» (в границу не входит). */
function slot(satelliteId: number, slotNumber: number, roll: number, mm: number, cycle = 1): S {
  const tn = `2026-06-17T10:${String(mm).padStart(2, "0")}:00`;
  const tk = `2026-06-17T10:${String(mm).padStart(2, "0")}:30`;
  return { satelliteId, slotNumber, cycle, tn, tk, roll };
}

/** Разбиение на полосы как множество множеств ключей — для сравнения, не зависящего от имён PolosaId. */
function partition(g: StripGrouping): string[][] {
  const byStrip = new Map<string, string[]>();
  for (const [key, strip] of g.idByKey) {
    const arr = byStrip.get(strip) ?? [];
    arr.push(key);
    byStrip.set(strip, arr);
  }
  return [...byStrip.values()].map((keys) => keys.slice().sort()).sort((a, b) => (a[0]! < b[0]! ? -1 : 1));
}

describe("groupStrips", () => {
  it("пустой вход → пустая разметка", () => {
    const g = groupStrips([], keyOf);
    expect(g.idByKey.size).toBe(0);
    expect(g.membersOf.size).toBe(0);
  });

  it("один слот → своя полоса", () => {
    const g = groupStrips([slot(1, 1, 5, 0)], keyOf);
    expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1");
    expect(g.membersOf.get("polosa:1:1:1")).toEqual(["1:1:1"]);
  });

  it("веер кренов в один момент → разные полосы; равный крен вдоль времени → одна полоса", () => {
    // КА 1: два нацеливания (крен 36.5 и 39.5, шаг 3° > ε), каждое снято в моменты mm=0 и mm=1.
    const slots = [
      slot(1, 1, 36.5, 0),
      slot(1, 2, 39.5, 0),
      slot(1, 3, 36.5, 1),
      slot(1, 4, 39.5, 1),
    ];
    expect(partition(groupStrips(slots, keyOf))).toEqual([
      ["1:1:1", "1:3:1"], // крен 36.5 вдоль времени
      ["1:2:1", "1:4:1"], // крен 39.5 вдоль времени
    ]);
  });

  it("смена витка (cycle) внутри одного крена и каденции НЕ рвёт полосу", () => {
    const slots = [
      slot(1, 1, 5, 0, 1),
      slot(1, 2, 5, 1, 1),
      slot(1, 3, 5, 2, 2), // виток сменился у экватора — но это та же полоса
      slot(1, 4, 5, 3, 2),
    ];
    expect(partition(groupStrips(slots, keyOf))).toEqual([
      ["1:1:1", "1:2:1", "1:3:2", "1:4:2"],
    ]);
  });

  it("большой разрыв по времени рвёт полосу на две (другой проход)", () => {
    const slots = [
      slot(1, 1, 5, 0),
      slot(1, 2, 5, 1),
      slot(1, 3, 5, 2),
      slot(1, 4, 5, 3),
      slot(1, 5, 5, 30), // разрыв ≫ каденции (1 мин) → новый проход
      slot(1, 6, 5, 31),
      slot(1, 7, 5, 32),
    ];
    expect(partition(groupStrips(slots, keyOf))).toEqual([
      ["1:1:1", "1:2:1", "1:3:1", "1:4:1"],
      ["1:5:1", "1:6:1", "1:7:1"],
    ]);
  });

  it("разные КА — разные полосы (даже при одном крене и времени)", () => {
    const slots = [slot(1, 1, 5, 0), slot(2, 1, 5, 0), slot(1, 2, 5, 1), slot(2, 2, 5, 1)];
    expect(partition(groupStrips(slots, keyOf))).toEqual([
      ["1:1:1", "1:2:1"],
      ["2:1:1", "2:2:1"],
    ]);
  });

  it("граница ε по крену: разрыв == 0.5 склеивает, 0.6 рвёт", () => {
    const same = groupStrips([slot(1, 1, 5.0, 0), slot(1, 2, 5 + ROLL_EPSILON, 1)], keyOf);
    expect(partition(same)).toEqual([["1:1:1", "1:2:1"]]);

    const broken = groupStrips([slot(1, 1, 5.0, 0), slot(1, 2, 5 + ROLL_EPSILON + 0.1, 1)], keyOf);
    expect(partition(broken)).toEqual([["1:1:1"], ["1:2:1"]]);
  });

  it("устойчивость к порядку входа: разбиение одинаково при перемешивании", () => {
    const slots = [slot(1, 1, 5, 0), slot(1, 2, 5, 1), slot(1, 3, 20, 0), slot(2, 1, 5, 0)];
    const a = partition(groupStrips(slots, keyOf));
    const b = partition(groupStrips([...slots].reverse(), keyOf));
    const c = partition(groupStrips([slots[2]!, slots[0]!, slots[3]!, slots[1]!], keyOf));
    expect(b).toEqual(a);
    expect(c).toEqual(a);
  });

  it("PolosaId = polosa:${ключ первого по времени члена}; membersOf — в порядке времени", () => {
    const g = groupStrips([slot(1, 2, 5, 1), slot(1, 1, 5, 0)], keyOf);
    expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1");
    expect(g.idByKey.get("1:2:1")).toBe("polosa:1:1:1");
    expect(g.membersOf.get("polosa:1:1:1")).toEqual(["1:1:1", "1:2:1"]);
  });

  it("NaN tn → синглтон без падения, не клеит и не рвёт валидных соседей", () => {
    const broken: S = { satelliteId: 1, slotNumber: 9, cycle: 1, tn: "битый", tk: "битый", roll: 5 };
    const slots = [slot(1, 1, 5, 0), broken, slot(1, 2, 5, 1)];
    const g = groupStrips(slots, keyOf);
    expect(g.idByKey.get("1:9:1")).toBe("polosa:1:9:1");
    expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1");
    expect(g.idByKey.get("1:2:1")).toBe("polosa:1:1:1");
  });
});

describe("pickStripAnchor", () => {
  const tn = (m: Record<string, number>) => (key: string) => m[key] ?? Number.NaN;

  it("берёт самый ранний по времени слот под курсором", () => {
    const got = pickStripAnchor(["b", "a", "c"], tn({ a: 200, b: 100, c: 300 }));
    expect(got).toBe("b");
  });

  it("равное время (веер кренов в один момент) → тай-брейк по ключу", () => {
    const got = pickStripAnchor(["b", "a"], tn({ a: 100, b: 100 }));
    expect(got).toBe("a");
  });

  it("слоты без валидного времени игнорируются", () => {
    const got = pickStripAnchor(["bad", "x"], tn({ x: 500 }));
    expect(got).toBe("x");
  });

  it("пустой / полностью невалидный вход → null", () => {
    expect(pickStripAnchor([], tn({}))).toBeNull();
    expect(pickStripAnchor(["bad"], tn({}))).toBeNull();
  });
});

describe("pickStripEnd", () => {
  // Полоса 1:1:1 = [1:1:1, 1:2:1, 1:3:1] по времени; другой КА 2 — отдельная полоса.
  const g = groupStrips(
    [slot(1, 1, 5, 0), slot(1, 2, 5, 1), slot(1, 3, 5, 2), slot(2, 1, 5, 0)],
    keyOf,
  );
  const anchorId = g.idByKey.get("1:1:1")!;

  it("берёт самого позднего по времени члена полосы под курсором", () => {
    expect(pickStripEnd(g, anchorId, ["1:1:1", "1:3:1"])).toBe("1:3:1");
    expect(pickStripEnd(g, anchorId, ["1:1:1", "1:2:1"])).toBe("1:2:1");
  });

  it("игнорирует ключи не из своей полосы (граница полосы)", () => {
    expect(pickStripEnd(g, anchorId, ["2:1:1"])).toBeNull();
    expect(pickStripEnd(g, anchorId, ["1:2:1", "2:1:1"])).toBe("1:2:1");
  });

  it("никого из полосы под курсором → null (клампим к прежнему концу)", () => {
    expect(pickStripEnd(g, anchorId, [])).toBeNull();
    expect(pickStripEnd(g, "polosa:нет", ["1:1:1"])).toBeNull();
  });
});
