import { STAGES, N, D_SPACING, X_SWAY, districtPosition, samplePath, cameraFor } from "./theme";

describe("strategy/theme — stage data", () => {
  it("exposes five stages and a matching N", () => {
    expect(STAGES).toHaveLength(5);
    expect(N).toBe(5);
  });

  it("has unique, well-formed stage descriptors", () => {
    const keys = STAGES.map((s) => s.key);
    expect(new Set(keys).size).toBe(keys.length);
    for (const s of STAGES) {
      expect(s.n).toMatch(/^\d{2}$/);
      expect(s.theme).toMatch(/^#[0-9A-Fa-f]{6}$/);
      expect(s.kicker.length).toBeGreaterThan(0);
    }
  });
});

describe("strategy/theme — districtPosition()", () => {
  it("sits on the floor (y=0), sways x by parity, steps z by -D_SPACING", () => {
    expect(districtPosition(0)).toEqual([-X_SWAY, 0, -0]);
    expect(districtPosition(1)).toEqual([X_SWAY, 0, -D_SPACING]);
    expect(districtPosition(2)).toEqual([-X_SWAY, 0, -2 * D_SPACING]);
    expect(districtPosition(3)).toEqual([X_SWAY, 0, -3 * D_SPACING]);
  });

  it("places even districts left and odd districts right", () => {
    for (let i = 0; i < N; i++) {
      const [x] = districtPosition(i);
      if (i % 2 === 0) expect(x).toBeLessThan(0);
      else expect(x).toBeGreaterThan(0);
    }
  });

  it("marches monotonically into -Z", () => {
    for (let i = 1; i < N; i++) {
      expect(districtPosition(i)[2]).toBeLessThan(districtPosition(i - 1)[2]);
    }
  });
});

describe("strategy/theme — samplePath()", () => {
  it("equals districtPosition() at integer indices", () => {
    for (let i = 0; i < N; i++) {
      const sampled = samplePath(i);
      const exact = districtPosition(i);
      sampled.forEach((v, k) => expect(v).toBeCloseTo(exact[k], 10));
    }
  });

  it("linearly interpolates between adjacent districts", () => {
    const [x, y, z] = samplePath(0.5);
    expect(x).toBeCloseTo(0, 10); // midpoint of -X_SWAY and +X_SWAY
    expect(y).toBeCloseTo(0, 10);
    expect(z).toBeCloseTo(-D_SPACING / 2, 10);
  });

  it("clamps indices below 0 to the first district", () => {
    expect(samplePath(-3)).toEqual(districtPosition(0));
  });

  it("clamps indices above N-1 to the last district", () => {
    expect(samplePath(99)).toEqual(districtPosition(N - 1));
  });

  it("never produces NaN across a fine sweep", () => {
    for (let p = 0; p <= 1.0001; p += 0.05) {
      const idx = p * (N - 1);
      for (const v of samplePath(idx)) expect(Number.isNaN(v)).toBe(false);
    }
  });
});

describe("strategy/theme — cameraFor()", () => {
  it("keeps the camera behind (+Z) and above (+Y) the active district", () => {
    for (let i = 0; i < N; i++) {
      const { pos, look } = cameraFor(i);
      const [, , dz] = districtPosition(i);
      expect(pos[2]).toBeGreaterThan(dz); // camera is back from the district
      expect(pos[1]).toBeGreaterThan(look[1]); // camera looks down
      expect(look[2]).toBeLessThan(pos[2]); // look target leads forward
    }
  });

  it("never produces NaN across a fine sweep", () => {
    for (let p = 0; p <= 1.0001; p += 0.05) {
      const { pos, look } = cameraFor(p * (N - 1));
      for (const v of [...pos, ...look]) expect(Number.isNaN(v)).toBe(false);
    }
  });
});
