import { describe, expect, it, vi } from "vitest";

import { createEmptySceneDocument } from "../../src/document/scene-document";
import {
  EditorSimulationController,
  type EditorSimulationUiSnapshot
} from "../../src/runtime-three/editor-simulation-controller";
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";

function createManualFrameController(options: {
  uiSnapshotIntervalSeconds?: number;
  onBuild?: () => void;
} = {}) {
  return new EditorSimulationController({
    uiSnapshotIntervalSeconds: options.uiSnapshotIntervalSeconds,
    requestAnimationFrame: () => 1,
    cancelAnimationFrame: () => undefined,
    buildRuntimeScene: (document, buildOptions) => {
      options.onBuild?.();
      return buildRuntimeSceneFromDocument(document, buildOptions);
    }
  });
}

describe("EditorSimulationController", () => {
  it("advances the editor clock only while playing", () => {
    const document = createEmptySceneDocument();
    document.time.dayLengthMinutes = 24;
    document.time.startTimeOfDayHours = 6;
    const controller = createManualFrameController();
    controller.updateInputs({
      document,
      loadedModelAssets: {}
    });

    const initialClock = controller.getUiSnapshot().clock;
    expect(initialClock?.timeOfDayHours).toBe(6);

    controller.play();
    controller.advance(60);

    const playingClock = controller.getUiSnapshot().clock;
    expect(playingClock?.timeOfDayHours).toBeGreaterThan(6);

    controller.pause();
    controller.advance(60);

    expect(controller.getUiSnapshot().clock).toEqual(playingClock);
  });

  it("publishes coarse UI snapshots at a bounded tick rate", () => {
    const document = createEmptySceneDocument();
    const controller = createManualFrameController({
      uiSnapshotIntervalSeconds: 0.5
    });
    const snapshots: EditorSimulationUiSnapshot[] = [];

    controller.updateInputs({
      document,
      loadedModelAssets: {}
    });
    controller.subscribeUiSnapshot((snapshot) => {
      snapshots.push(snapshot);
    });
    snapshots.length = 0;

    controller.play();
    expect(snapshots).toHaveLength(1);
    snapshots.length = 0;

    controller.advance(0.2);
    controller.advance(0.2);
    expect(snapshots).toHaveLength(0);

    controller.advance(0.1);
    expect(snapshots).toHaveLength(1);
  });

  it("rebuilds the cached base simulation scene only when inputs change", () => {
    let buildCount = 0;
    const document = createEmptySceneDocument();
    const loadedModelAssets = {};
    const controller = createManualFrameController({
      onBuild: () => {
        buildCount += 1;
      }
    });

    controller.updateInputs({
      document,
      loadedModelAssets
    });
    expect(buildCount).toBe(1);

    controller.play();
    controller.advance(0.1);
    controller.advance(0.1);
    expect(buildCount).toBe(1);

    controller.updateInputs({
      document,
      loadedModelAssets
    });
    expect(buildCount).toBe(1);

    const renamedDocument = {
      ...document,
      name: "Rebuilt Scene"
    };
    controller.updateInputs({
      document: renamedDocument,
      loadedModelAssets
    });
    expect(buildCount).toBe(2);

    const retimedDocument = {
      ...renamedDocument,
      time: {
        ...renamedDocument.time,
        dayLengthMinutes: 12
      }
    };
    controller.updateInputs({
      document: retimedDocument,
      loadedModelAssets
    });
    expect(buildCount).toBe(3);

    controller.updateInputs({
      document: retimedDocument,
      loadedModelAssets: {}
    });
    expect(buildCount).toBe(4);
  });

  it("emits frame updates without publishing a UI snapshot every frame", () => {
    const document = createEmptySceneDocument();
    const controller = createManualFrameController({
      uiSnapshotIntervalSeconds: 1
    });
    const frameListener = vi.fn();
    const uiListener = vi.fn();

    controller.updateInputs({
      document,
      loadedModelAssets: {}
    });
    controller.subscribeFrame(frameListener);
    controller.subscribeUiSnapshot(uiListener);
    frameListener.mockClear();
    uiListener.mockClear();

    controller.play();
    frameListener.mockClear();
    uiListener.mockClear();
    controller.advance(0.25);
    controller.advance(0.25);

    expect(frameListener).toHaveBeenCalledTimes(2);
    expect(uiListener).not.toHaveBeenCalled();
  });
});
