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

import {
  createActorControlTargetRef,
  createProjectGlobalControlTargetRef,
  createLightControlTargetRef,
  createFollowActorPathControlEffect,
  createPlayActorAnimationControlEffect,
  createSetActorPresenceControlEffect,
  createSetLightIntensityControlEffect
} from "../../src/controls/control-surface";
import { createScenePath } from "../../src/document/paths";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import {
  createNpcEntity,
  createPointLightEntity
} from "../../src/entities/entity-instances";
import { createProjectScheduleRoutine } from "../../src/scheduler/project-scheduler";
import { createProjectSequence } from "../../src/sequencer/project-sequences";
import {
  applyRuntimeProjectScheduleToControlState,
  resolveRuntimeProjectScheduleState
} from "../../src/runtime-three/runtime-project-scheduler";
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";
import {
  commitRuntimeScheduleSyncResult,
  createRuntimeScheduleSyncContext,
  syncRuntimeSceneScheduleToClock
} from "../../src/runtime-three/runtime-schedule-sync";
import { BoxGeometry } from "three";
import { createFixtureLoadedModelAssetFromGeometry } from "../helpers/model-collider-fixtures";

describe("runtime schedule sync", () => {
  it("matches direct scheduler resolution while applying actor path and animation state", () => {
    const actorTarget = createActorControlTargetRef("actor-patroller");
    const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry(
      "asset-npc-patroller",
      new BoxGeometry(0.8, 1.8, 0.6)
    );
    asset.metadata.animationNames = ["Walk"];
    const npc = createNpcEntity({
      id: "entity-npc-patroller",
      actorId: actorTarget.actorId,
      modelAssetId: asset.id,
      yawDegrees: 15
    });
    const path = createScenePath({
      id: "path-patrol",
      points: [
        {
          id: "path-point-start",
          position: {
            x: 0,
            y: 0,
            z: 0
          }
        },
        {
          id: "path-point-end",
          position: {
            x: 8,
            y: 0,
            z: 0
          }
        }
      ]
    });
    const document = createEmptySceneDocument();
    document.assets[asset.id] = asset;
    document.entities[npc.id] = npc;
    document.paths[path.id] = path;
    document.scheduler.routines["routine-patrol"] =
      createProjectScheduleRoutine({
        id: "routine-patrol",
        title: "Patrolling",
        target: actorTarget,
        startHour: 9,
        endHour: 13,
        effects: [
          createSetActorPresenceControlEffect({
            target: actorTarget,
            active: true
          }),
          createPlayActorAnimationControlEffect({
            target: actorTarget,
            clipName: "Walk",
            loop: true
          }),
          createFollowActorPathControlEffect({
            target: actorTarget,
            pathId: path.id,
            speed: 2,
            loop: false,
            progressMode: "deriveFromTime"
          })
        ]
      });
    const runtimeScene = buildRuntimeSceneFromDocument(document, {
      runtimeClock: {
        timeOfDayHours: 6,
        dayCount: 0,
        dayLengthMinutes: 24
      },
      loadedModelAssets: {
        [asset.id]: loadedAsset
      }
    });
    const context = createRuntimeScheduleSyncContext(runtimeScene);
    const clock = {
      timeOfDayHours: 11,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    const directResolvedScheduler = resolveRuntimeProjectScheduleState({
      scheduler: runtimeScene.scheduler.document,
      sequences: runtimeScene.sequences,
      actorIds: runtimeScene.npcDefinitions.map(
        (definition) => definition.actorId
      ),
      dayNumber: clock.dayCount + 1,
      timeOfDayHours: clock.timeOfDayHours,
      pathsById: context.pathsById
    });
    const directResolvedControl = applyRuntimeProjectScheduleToControlState(
      runtimeScene.control.resolved,
      directResolvedScheduler,
      runtimeScene.control.baselineResolved
    );

    const result = syncRuntimeSceneScheduleToClock({
      runtimeScene,
      clock,
      context
    });
    commitRuntimeScheduleSyncResult(runtimeScene, result);

    expect(result.resolvedScheduler).toEqual(directResolvedScheduler);
    expect(result.resolvedControl).toEqual(directResolvedControl);
    expect(result.npcChanges).toHaveLength(1);
    expect(result.npcEntityCollectionChanged).toBe(true);
    expect(runtimeScene.npcDefinitions[0]).toEqual(
      expect.objectContaining({
        entityId: npc.id,
        activeRoutineTitle: "Patrolling",
        animationClipName: "Walk",
        yawDegrees: 90,
        position: {
          x: 4,
          y: 0,
          z: 0
        },
        resolvedPath: expect.objectContaining({
          pathId: path.id,
          progress: 0.5
        })
      })
    );
    expect(runtimeScene.entities.npcs).toEqual([
      expect.objectContaining({
        entityId: npc.id,
        activeRoutineTitle: "Patrolling",
        animationClipName: "Walk",
        position: {
          x: 4,
          y: 0,
          z: 0
        }
      })
    ]);
  });

  it("reuses scene-stable scheduler inputs and leaves NPC collections untouched when output is unchanged", () => {
    const npc = createNpcEntity({
      id: "entity-npc-stable",
      actorId: "actor-stable"
    });
    const document = createEmptySceneDocument();
    document.entities[npc.id] = npc;
    const runtimeScene = buildRuntimeSceneFromDocument(document);
    const context = createRuntimeScheduleSyncContext(runtimeScene);
    const clock = {
      timeOfDayHours: 6,
      dayCount: 0,
      dayLengthMinutes: 24
    };

    const firstResult = syncRuntimeSceneScheduleToClock({
      runtimeScene,
      clock,
      context
    });
    const npcEntities = runtimeScene.entities.npcs;
    const colliders = runtimeScene.colliders;
    const pathsById = context.pathsById;
    const actorStatesByActorId = context.actorStatesByActorId;
    const actorIds = context.actorIds;
    const secondResult = syncRuntimeSceneScheduleToClock({
      runtimeScene,
      clock,
      context
    });

    expect(firstResult.npcChanges).toHaveLength(0);
    expect(secondResult.npcChanges).toHaveLength(0);
    expect(context.pathsById).toBe(pathsById);
    expect(context.actorIds).toBe(actorIds);
    expect(context.actorStatesByActorId).toBe(actorStatesByActorId);
    expect(runtimeScene.entities.npcs).toBe(npcEntities);
    expect(runtimeScene.colliders).toBe(colliders);
  });

  it("resolves scheduled controls and impulse routines through the shared helper", () => {
    const pointLight = createPointLightEntity({
      id: "entity-point-light-night-lamp",
      intensity: 1.25
    });
    const document = createEmptySceneDocument();
    document.entities[pointLight.id] = pointLight;
    document.sequences.sequences["sequence-transition"] = createProjectSequence({
      id: "sequence-transition",
      title: "Transition",
      effects: [
        {
          stepClass: "impulse",
          type: "startSceneTransition",
          targetSceneId: "scene-house",
          targetEntryEntityId: "entry-house"
        }
      ]
    });
    document.scheduler.routines["routine-night-lamp"] =
      createProjectScheduleRoutine({
        id: "routine-night-lamp",
        title: "Night Lamp",
        target: createLightControlTargetRef("pointLight", pointLight.id),
        startHour: 20,
        endHour: 4,
        effect: createSetLightIntensityControlEffect({
          target: createLightControlTargetRef("pointLight", pointLight.id),
          intensity: 3.5
        })
      });
    document.scheduler.routines["routine-transition"] =
      createProjectScheduleRoutine({
        id: "routine-transition",
        title: "Transition Window",
        target: createProjectGlobalControlTargetRef(),
        startHour: 20,
        endHour: 22,
        sequenceId: "sequence-transition",
        effects: []
      });
    const runtimeScene = buildRuntimeSceneFromDocument(document);
    const context = createRuntimeScheduleSyncContext(runtimeScene);
    const clock = {
      timeOfDayHours: 21,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    const directResolvedScheduler = resolveRuntimeProjectScheduleState({
      scheduler: runtimeScene.scheduler.document,
      sequences: runtimeScene.sequences,
      actorIds: context.actorIds,
      dayNumber: clock.dayCount + 1,
      timeOfDayHours: clock.timeOfDayHours,
      pathsById: context.pathsById
    });
    const directResolvedControl = applyRuntimeProjectScheduleToControlState(
      runtimeScene.control.resolved,
      directResolvedScheduler,
      runtimeScene.control.baselineResolved
    );

    const result = syncRuntimeSceneScheduleToClock({
      runtimeScene,
      clock,
      context
    });

    expect(result.resolvedScheduler).toEqual(directResolvedScheduler);
    expect(result.resolvedControl).toEqual(directResolvedControl);
    expect(result.resolvedScheduler.impulses).toEqual([
      expect.objectContaining({
        routineId: "routine-transition",
        effects: [
          expect.objectContaining({
            type: "startSceneTransition",
            targetSceneId: "scene-house",
            targetEntryEntityId: "entry-house"
          })
        ]
      })
    ]);
    expect(result.nextActiveImpulseRoutineIds).toEqual(
      new Set(["routine-transition"])
    );
    expect(result.resolvedControl.channels).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "lightIntensity",
          value: 3.5,
          source: {
            kind: "scheduler",
            scheduleId: "routine-night-lamp"
          }
        })
      ])
    );
  });
});
