import { describe, expect, it, vi } from "vitest";
import { bossDeathDespawnSeconds } from "../game/bossDeath";
import {
  OUTCOME_RENDER_TAIL_SECONDS,
  activePlayfieldKind,
  consumeSimulationSteps,
  outcomeElapsedAfterPhaseChange,
  resetSceneClockForMode,
  sceneCanvasFrameloop,
  sceneFrameIntervalMs,
  selectSceneRenderMode,
  startSceneFrameLoop,
  summarizeFramePerformance,
  type SceneFrameSample,
} from "./sceneFramePolicy";

function createFakeFrames() {
  let nextHandle = 1;
  const callbacks = new Map<number, FrameRequestCallback>();
  return {
    requestFrame(callback: FrameRequestCallback) {
      const handle = nextHandle++;
      callbacks.set(handle, callback);
      return handle;
    },
    cancelFrame(handle: number) {
      callbacks.delete(handle);
    },
    runNext(now: number) {
      const entry = callbacks.entries().next().value as [number, FrameRequestCallback] | undefined;
      if (!entry) return false;
      callbacks.delete(entry[0]);
      entry[1](now);
      return true;
    },
    pending() {
      return callbacks.size;
    },
  };
}

describe("scene render policy", () => {
  it("selects active, outcome, static, and suspended modes", () => {
    expect(selectSceneRenderMode({ phase: "combat", paused: false, visible: true, outcomeTailComplete: false })).toBe("active");
    expect(selectSceneRenderMode({ phase: "victory", paused: false, visible: true, outcomeTailComplete: false })).toBe("outcome");
    expect(selectSceneRenderMode({ phase: "victory", paused: false, visible: true, outcomeTailComplete: true })).toBe("static");
    expect(selectSceneRenderMode({ phase: "briefing", paused: false, visible: true, outcomeTailComplete: false })).toBe("static");
    expect(selectSceneRenderMode({ phase: "combat", paused: true, visible: true, outcomeTailComplete: false })).toBe("static");
    expect(selectSceneRenderMode({ phase: "combat", paused: false, visible: false, outcomeTailComplete: false })).toBe("suspended");
  });

  it("keeps simulation fixed at 10 Hz and caps delayed catch-up at three steps", () => {
    expect(consumeSimulationSteps(0, 0.09)).toEqual({ steps: 0, remainderSeconds: 0.09 });
    const oneStep = consumeSimulationSteps(0.09, 0.02);
    expect(oneStep.steps).toBe(1);
    expect(oneStep.remainderSeconds).toBeCloseTo(0.01);

    const delayed = consumeSimulationSteps(0.09, 1);
    expect(delayed.steps).toBe(3);
    expect(delayed.remainderSeconds).toBeCloseTo(0.04);
  });

  it("has no continuous interval for static or suspended scenes", () => {
    expect(sceneCanvasFrameloop("active")).toBe("demand");
    expect(sceneCanvasFrameloop("outcome")).toBe("demand");
    expect(sceneCanvasFrameloop("static")).toBe("demand");
    expect(sceneCanvasFrameloop("suspended")).toBe("never");
    expect(sceneFrameIntervalMs("static")).toBeNull();
    expect(sceneFrameIntervalMs("suspended")).toBeNull();
  });

  it("resets the demand clock before active and outcome frame requests", () => {
    const start = vi.fn();
    for (const mode of ["static", "suspended", "active", "outcome"] as const) {
      resetSceneClockForMode({ start }, mode);
    }
    expect(start).toHaveBeenCalledTimes(2);
  });

  it("mounts only the selected special playfield", () => {
    expect(activePlayfieldKind("boss")).toBeNull();
    expect(activePlayfieldKind("hockey-healing")).toBe("hockey-healing");
    expect(activePlayfieldKind("blockbreaker")).toBe("blockbreaker");
    expect(activePlayfieldKind("aether-assault")).toBe("aether-assault");
    expect(activePlayfieldKind("hockey-healing-pvp")).toBe("hockey-healing-pvp");
  });

  it("keeps outcome rendering longer than the longest authored boss death", () => {
    expect(OUTCOME_RENDER_TAIL_SECONDS).toBeGreaterThan(bossDeathDespawnSeconds("gravehorn-triceratops"));
  });

  it("restarts the outcome tail when the outcome phase identity changes", () => {
    expect(outcomeElapsedAfterPhaseChange("victory", "victory", 3.5)).toBe(3.5);
    expect(outcomeElapsedAfterPhaseChange("victory", "intermission", 3.5)).toBe(0);
    expect(outcomeElapsedAfterPhaseChange("defeat", null, 3.5)).toBe(0);
  });
});

describe("scene frame request loop", () => {
  it("caps active rendering at 60 FPS", () => {
    const fake = createFakeFrames();
    const samples: SceneFrameSample[] = [];
    const stop = startSceneFrameLoop({
      mode: "active",
      requestFrame: fake.requestFrame,
      cancelFrame: fake.cancelFrame,
      onFrame: (sample) => samples.push(sample),
    });

    expect(fake.runNext(0)).toBe(true);
    expect(fake.runNext(8)).toBe(true);
    expect(fake.runNext(16.7)).toBe(true);
    expect(fake.runNext(25)).toBe(true);
    expect(fake.runNext(33.4)).toBe(true);
    expect(samples).toHaveLength(3);
    expect(samples.map((sample) => sample.elapsedSeconds)).toEqual([0, 0.0167, 0.0167]);

    stop();
    expect(fake.pending()).toBe(0);
  });

  it("renders an outcome for seven animation seconds without simulation ownership", () => {
    const fake = createFakeFrames();
    const completed = vi.fn();
    const samples: SceneFrameSample[] = [];
    startSceneFrameLoop({
      mode: "outcome",
      requestFrame: fake.requestFrame,
      cancelFrame: fake.cancelFrame,
      onFrame: (sample) => samples.push(sample),
      onOutcomeComplete: completed,
    });

    fake.runNext(0);
    let now = 0;
    while (fake.pending() && now < 8_000) {
      now += 1000 / 30;
      fake.runNext(now);
    }

    expect(completed).toHaveBeenCalledTimes(1);
    expect(samples.at(-1)?.outcomeElapsedSeconds).toBeGreaterThanOrEqual(OUTCOME_RENDER_TAIL_SECONDS);
    expect(fake.pending()).toBe(0);
  });

  it("finishes an outcome after seven visible wall seconds when RAF is throttled", () => {
    const fake = createFakeFrames();
    const completed = vi.fn();
    const samples: SceneFrameSample[] = [];
    startSceneFrameLoop({
      mode: "outcome",
      requestFrame: fake.requestFrame,
      cancelFrame: fake.cancelFrame,
      onFrame: (sample) => samples.push(sample),
      onOutcomeComplete: completed,
    });

    fake.runNext(0);
    for (let now = 1_000; now <= 7_000; now += 1_000) fake.runNext(now);

    expect(completed).toHaveBeenCalledTimes(1);
    expect(samples.at(-1)?.outcomeElapsedSeconds).toBe(7);
    expect(fake.pending()).toBe(0);
  });

  it("cancels immediately and resumes without hidden-time catch-up", () => {
    const fake = createFakeFrames();
    const beforeHide: SceneFrameSample[] = [];
    const stop = startSceneFrameLoop({
      mode: "active",
      requestFrame: fake.requestFrame,
      cancelFrame: fake.cancelFrame,
      onFrame: (sample) => beforeHide.push(sample),
    });
    fake.runNext(100);
    fake.runNext(117);
    stop();
    expect(fake.pending()).toBe(0);

    const afterResume: SceneFrameSample[] = [];
    const stopResumed = startSceneFrameLoop({
      mode: "active",
      requestFrame: fake.requestFrame,
      cancelFrame: fake.cancelFrame,
      onFrame: (sample) => afterResume.push(sample),
    });
    fake.runNext(60_000);

    expect(beforeHide.at(-1)?.elapsedSeconds).toBeCloseTo(0.017);
    expect(afterResume[0]).toMatchObject({
      elapsedMs: 0,
      elapsedSeconds: 0,
    });
    stopResumed();
  });
});

describe("performance summaries", () => {
  it("uses mode-specific targets and only counts active missed frames", () => {
    const active = summarizeFramePerformance("active", [16, 18, 20]);
    expect(active.targetFps).toBe(60);
    expect(active.overBudget).toBe(1);

    const outcome = summarizeFramePerformance("outcome", [32, 35]);
    expect(outcome.targetFps).toBe(30);
    expect(outcome.budgetMs).toBeCloseTo(1000 / 30);
    expect(outcome.overBudget).toBe(0);

    expect(summarizeFramePerformance("static", [])).toMatchObject({ targetFps: 0, budgetMs: null, samples: 0, overBudget: 0 });
    expect(summarizeFramePerformance("suspended", [])).toMatchObject({ targetFps: 0, budgetMs: null, samples: 0, overBudget: 0 });
  });
});
