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

import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
import { createDefaultWorldSettings } from "../../src/document/world-settings";
import {
  advanceRuntimeClockState,
  createRuntimeClockState,
  hasTimeWindowJustEnded,
  hasTimeWindowJustStarted,
  isWithinTimeWindow,
  resolveRuntimeDayPhase,
  resolveRuntimeDayNightWorldState,
  resolveRuntimeTimeState
} from "../../src/runtime-three/runtime-project-time";

describe("runtime project time", () => {
  it("advances and wraps the global clock using the authored day duration", () => {
    const settings = createDefaultProjectTimeSettings();
    settings.startDayNumber = 3;
    settings.startTimeOfDayHours = 23.5;
    settings.dayLengthMinutes = 24;
    const clock = createRuntimeClockState(settings);

    const advancedClock = advanceRuntimeClockState(clock, 60);

    expect(advancedClock.timeOfDayHours).toBeCloseTo(0.5);
    expect(advancedClock.dayCount).toBe(3);
    expect(advancedClock.dayLengthMinutes).toBe(24);
  });

  it("resolves reusable runtime day semantics from the authored sun windows", () => {
    const settings = createDefaultProjectTimeSettings();
    settings.startDayNumber = 4;
    settings.dayLengthMinutes = 30;
    settings.sunriseTimeOfDayHours = 7;
    settings.sunsetTimeOfDayHours = 20;
    settings.dawnDurationHours = 2;
    settings.duskDurationHours = 2;

    const resolvedNight = resolveRuntimeTimeState(settings, {
      timeOfDayHours: 2,
      dayCount: 4,
      dayLengthMinutes: 30
    });
    const resolvedDawn = resolveRuntimeTimeState(settings, {
      timeOfDayHours: 6.5,
      dayCount: 4,
      dayLengthMinutes: 30
    });
    const resolvedDay = resolveRuntimeTimeState(settings, {
      timeOfDayHours: 12,
      dayCount: 4,
      dayLengthMinutes: 30
    });
    const resolvedDusk = resolveRuntimeTimeState(settings, {
      timeOfDayHours: 20.5,
      dayCount: 4,
      dayLengthMinutes: 30
    });

    expect(resolveRuntimeDayPhase(settings, 2)).toBe("night");
    expect(resolveRuntimeDayPhase(settings, 6.5)).toBe("dawn");
    expect(resolveRuntimeDayPhase(settings, 12)).toBe("day");
    expect(resolveRuntimeDayPhase(settings, 20.5)).toBe("dusk");

    expect(resolvedNight).toMatchObject({
      timeOfDayHours: 2,
      dayCount: 4,
      dayLengthMinutes: 30,
      dayPhase: "night",
      isNight: true
    });
    expect(resolvedDawn.dayPhase).toBe("dawn");
    expect(resolvedDawn.isNight).toBe(false);
    expect(resolvedDay.dayPhase).toBe("day");
    expect(resolvedDay.isNight).toBe(false);
    expect(resolvedDusk.dayPhase).toBe("dusk");
  });

  it("handles forward time windows across midnight", () => {
    expect(isWithinTimeWindow(22, 2, 23.5)).toBe(true);
    expect(isWithinTimeWindow(22, 2, 1.5)).toBe(true);
    expect(isWithinTimeWindow(22, 2, 12)).toBe(false);

    expect(hasTimeWindowJustStarted(21.5, 23.5, 22, 2)).toBe(true);
    expect(hasTimeWindowJustStarted(23.5, 1, 22, 2)).toBe(false);
    expect(hasTimeWindowJustEnded(23.5, 2.5, 22, 2)).toBe(true);

    expect(hasTimeWindowJustStarted(21, 3, 22, 2)).toBe(true);
    expect(hasTimeWindowJustEnded(21, 3, 22, 2)).toBe(true);
  });

  it("derives authored dawn, day, and night lighting from the global time profile", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    world.background = {
      mode: "verticalGradient",
      topColorHex: "#88ccff",
      bottomColorHex: "#f2b774"
    };
    world.timeOfDay.night.background = {
      mode: "verticalGradient",
      topColorHex: "#07101f",
      bottomColorHex: "#18253b"
    };
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 20;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;

    const noon = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 12,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const preSunrise = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 6.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const dawn = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 7.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const lateDawn = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 7.9,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const postSunset = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 20.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const midnight = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 0,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(midnight.sunLight.intensity).toBeLessThan(noon.sunLight.intensity);
    expect(midnight.ambientLight.intensity).toBeLessThan(
      noon.ambientLight.intensity
    );
    expect(preSunrise.ambientLight.intensity).toBeGreaterThan(
      midnight.ambientLight.intensity
    );
    expect(preSunrise.ambientLight.intensity).toBeLessThan(
      noon.ambientLight.intensity
    );
    expect(dawn.sunLight.colorHex).not.toBe(noon.sunLight.colorHex);
    expect(dawn.ambientLight.intensity).toBeGreaterThan(
      preSunrise.ambientLight.intensity
    );
    expect(postSunset.ambientLight.intensity).toBeLessThan(
      noon.ambientLight.intensity
    );
    expect(postSunset.ambientLight.intensity).toBeGreaterThan(
      midnight.ambientLight.intensity
    );
    expect(midnight.moonLight?.intensity ?? 0).toBeGreaterThan(0);
    expect(preSunrise.moonLight?.intensity ?? 0).toBeGreaterThan(0);
    expect(dawn.moonLight?.intensity ?? 0).toBeGreaterThan(0);
    expect(lateDawn.moonLight?.intensity ?? 0).toBeLessThan(
      dawn.moonLight?.intensity ?? 0
    );
    expect(postSunset.moonLight?.intensity ?? 0).toBeGreaterThan(0);
    expect(noon.moonLight).toBeNull();
    expect(noon.sunLight.direction.y).toBeGreaterThan(0);
    expect(midnight.sunLight.direction.y).toBeLessThan(0);

    if (
      preSunrise.background.mode !== "verticalGradient" ||
      dawn.background.mode !== "verticalGradient" ||
      noon.background.mode !== "verticalGradient" ||
      midnight.background.mode !== "verticalGradient"
    ) {
      throw new Error("Expected a gradient background for the day/night test.");
    }

    expect(preSunrise.background.topColorHex).not.toBe(
      midnight.background.topColorHex
    );
    expect(dawn.background.topColorHex).not.toBe(noon.background.topColorHex);
    expect(midnight.background.topColorHex).not.toBe(
      noon.background.topColorHex
    );
    expect(midnight.background.bottomColorHex).not.toBe(
      noon.background.bottomColorHex
    );
  });

  it("starts the visible sun and moon journeys at the end of dawn and dusk", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 20;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;

    const midDawn = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 7.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const dawnEnd = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 8,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const preDawnEnd = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 7.99,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const duskEnd = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 21,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const lateDusk = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 21.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(midDawn.sunLight.direction.y).toBeLessThan(-0.05);
    expect(Math.abs(dawnEnd.sunLight.direction.y)).toBeLessThan(0.05);
    expect(Math.abs(duskEnd.sunLight.direction.y)).toBeLessThan(0.05);
    expect(lateDusk.sunLight.direction.y).toBeLessThan(-0.05);
    expect(midDawn.moonLight?.direction.y ?? 0).toBeGreaterThan(0.05);
    expect(Math.abs(preDawnEnd.moonLight?.direction.y ?? 1)).toBeLessThan(0.05);
    expect(Math.abs(duskEnd.moonLight?.direction.y ?? 1)).toBeLessThan(0.05);
    expect(lateDusk.moonLight?.direction.y ?? 0).toBeGreaterThan(0.05);
  });

  it("uses the authored peak altitude as the 12-hour baseline orbit height", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 19;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;
    world.celestialOrbits.sun.peakAltitudeDegrees = 28;
    world.celestialOrbits.moon.peakAltitudeDegrees = 42;

    const sunPeak = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 14,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const moonPeak = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 2,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const expectedSunPeakY = Math.sin((28 * Math.PI) / 180);
    const expectedMoonPeakY = Math.sin((42 * Math.PI) / 180);

    expect(sunPeak.sunLight.direction.y).toBeCloseTo(expectedSunPeakY, 3);
    expect(moonPeak.moonLight?.direction.y ?? 0).toBeCloseTo(
      expectedMoonPeakY,
      3
    );
    expect(sunPeak.sunLight.direction.y).toBeLessThan(0.55);
    expect(moonPeak.moonLight?.direction.y ?? 0).toBeLessThan(0.7);
  });

  it("changes orbit height and sideways horizon position from day and night length", () => {
    const world = createDefaultWorldSettings();
    const shortDay = createDefaultProjectTimeSettings();
    const longDay = createDefaultProjectTimeSettings();
    world.celestialOrbits.sun.azimuthDegrees = 0;
    world.celestialOrbits.sun.peakAltitudeDegrees = 45;
    world.celestialOrbits.moon.azimuthDegrees = 0;
    world.celestialOrbits.moon.peakAltitudeDegrees = 32;
    shortDay.sunriseTimeOfDayHours = 8;
    shortDay.sunsetTimeOfDayHours = 16;
    shortDay.dawnDurationHours = 2;
    shortDay.duskDurationHours = 2;
    longDay.sunriseTimeOfDayHours = 4;
    longDay.sunsetTimeOfDayHours = 20;
    longDay.dawnDurationHours = 2;
    longDay.duskDurationHours = 2;

    const shortDayPeak = resolveRuntimeDayNightWorldState(world, shortDay, {
      timeOfDayHours: 13,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const longDayPeak = resolveRuntimeDayNightWorldState(world, longDay, {
      timeOfDayHours: 13,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const shortDaySunrise = resolveRuntimeDayNightWorldState(world, shortDay, {
      timeOfDayHours: 9,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const longDaySunrise = resolveRuntimeDayNightWorldState(world, longDay, {
      timeOfDayHours: 5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const shortNightMoonPeak = resolveRuntimeDayNightWorldState(
      world,
      longDay,
      {
        timeOfDayHours: 1,
        dayCount: 0,
        dayLengthMinutes: 24
      }
    );
    const longNightMoonPeak = resolveRuntimeDayNightWorldState(
      world,
      shortDay,
      {
        timeOfDayHours: 1,
        dayCount: 0,
        dayLengthMinutes: 24
      }
    );

    expect(shortDayPeak.sunLight.direction.y).toBeLessThan(
      longDayPeak.sunLight.direction.y
    );
    expect(Math.abs(shortDaySunrise.sunLight.direction.y)).toBeLessThan(0.05);
    expect(Math.abs(longDaySunrise.sunLight.direction.y)).toBeLessThan(0.05);
    expect(shortDaySunrise.sunLight.direction.x).toBeGreaterThan(0.05);
    expect(longDaySunrise.sunLight.direction.x).toBeLessThan(-0.05);
    expect(longNightMoonPeak.moonLight?.direction.y ?? 0).toBeGreaterThan(
      shortNightMoonPeak.moonLight?.direction.y ?? 0
    );
  });

  it("uses independent authored orbit settings for the moon path", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 19;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;
    world.celestialOrbits.sun.azimuthDegrees = 180;
    world.celestialOrbits.moon.azimuthDegrees = 90;
    world.celestialOrbits.sun.peakAltitudeDegrees = 40;
    world.celestialOrbits.moon.peakAltitudeDegrees = 25;

    const sunPeak = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 14,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const moonPeak = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 2,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(sunPeak.sunLight.direction.x).toBeLessThan(-0.7);
    expect(Math.abs(sunPeak.sunLight.direction.z)).toBeLessThan(0.05);
    expect(sunPeak.sunLight.direction.y).toBeCloseTo(
      Math.sin((40 * Math.PI) / 180),
      3
    );
    expect(moonPeak.moonLight?.direction.z ?? 0).toBeGreaterThan(0.85);
    expect(Math.abs(moonPeak.moonLight?.direction.x ?? 1)).toBeLessThan(0.05);
    expect(moonPeak.moonLight?.direction.y ?? 0).toBeCloseTo(
      Math.sin((25 * Math.PI) / 180),
      3
    );
  });

  it("uses the scene night image as a runtime overlay when the night background is an image", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 20;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;
    world.timeOfDay.night.background = {
      mode: "image",
      assetId: "asset-night-sky",
      environmentIntensity: 0.42
    };

    const preSunrise = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 6.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const postSunset = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 20.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const midnight = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 0,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(preSunrise.nightBackgroundOverlay?.assetId).toBe("asset-night-sky");
    expect(preSunrise.nightBackgroundOverlay?.opacity ?? 0).toBeGreaterThan(0);
    expect(postSunset.nightBackgroundOverlay?.opacity ?? 0).toBeGreaterThan(0);
    expect(midnight.nightBackgroundOverlay?.opacity ?? 0).toBeCloseTo(1);
  });

  it("falls back to day and night image maps when dusk image mode has no assigned asset", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 20;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;
    world.background = {
      mode: "image",
      assetId: "asset-day-sky",
      environmentIntensity: 0.8
    };
    world.timeOfDay.dusk.background = {
      mode: "image",
      assetId: "",
      environmentIntensity: 0.55
    };
    world.timeOfDay.night.background = {
      mode: "image",
      assetId: "asset-night-sky",
      environmentIntensity: 0.35
    };

    const dusk = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 20.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(dusk.background).toEqual({
      mode: "image",
      assetId: "asset-day-sky",
      environmentIntensity: 0.8
    });
    expect(dusk.nightBackgroundOverlay?.assetId).toBe("asset-night-sky");
    expect(dusk.nightBackgroundOverlay?.opacity ?? 0).toBeGreaterThan(0);
  });

  it("prefers a twilight-specific image background when one is authored", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 20;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;
    world.background = {
      mode: "image",
      assetId: "asset-day-sky",
      environmentIntensity: 0.8
    };
    world.timeOfDay.dawn.background = {
      mode: "image",
      assetId: "asset-dawn-sky",
      environmentIntensity: 0.6
    };
    world.timeOfDay.night.background = {
      mode: "image",
      assetId: "asset-night-sky",
      environmentIntensity: 0.35
    };

    const dawn = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 7.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(dawn.background).toEqual({
      mode: "image",
      assetId: "asset-dawn-sky",
      environmentIntensity: 0.6
    });
    expect(dawn.nightBackgroundOverlay?.assetId).toBe("asset-night-sky");
  });

  it("leaves scene lighting untouched when the scene disables project time influence", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    world.projectTimeLightingEnabled = false;

    const resolved = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 0,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(resolved.ambientLight).toEqual(world.ambientLight);
    expect(resolved.sunLight).toEqual(world.sunLight);
    expect(resolved.background).toEqual(world.background);
    expect(resolved.moonLight).toBeNull();
  });

  it("keeps shader sky mode active across time phases without reusing image overlays", () => {
    const world = createDefaultWorldSettings();
    const time = createDefaultProjectTimeSettings();
    world.background = {
      mode: "shader"
    };
    time.sunriseTimeOfDayHours = 7;
    time.sunsetTimeOfDayHours = 20;
    time.dawnDurationHours = 2;
    time.duskDurationHours = 2;

    const dusk = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 20.5,
      dayCount: 0,
      dayLengthMinutes: 24
    });
    const midnight = resolveRuntimeDayNightWorldState(world, time, {
      timeOfDayHours: 0,
      dayCount: 0,
      dayLengthMinutes: 24
    });

    expect(dusk.background).toEqual({
      mode: "shader"
    });
    expect(midnight.background).toEqual({
      mode: "shader"
    });
    expect(dusk.nightBackgroundOverlay).toBeNull();
    expect(midnight.nightBackgroundOverlay).toBeNull();
  });
});
