import { assert, beforeEach, describe, test } from "vitest";
import type { EFTimegroup } from "./EFTimegroup.js";
import {
  type AnimatableElement,
  updateAnimations,
} from "./updateAnimations.js";

import "./EFTimegroup.js";

// Import the constant for tests that need to check animation precision

beforeEach(() => {
  // Clean up DOM
  while (document.body.children.length) {
    document.body.children[0]?.remove();
  }
  // Clean up localStorage
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (typeof key !== "string") continue;
    localStorage.removeItem(key);
  }
});

function createTestElement(
  props: Partial<AnimatableElement> = {},
): AnimatableElement {
  const element = document.createElement("div") as unknown as AnimatableElement;
  // Override readonly properties for testing
  Object.defineProperty(element, "currentTimeMs", {
    value: props.currentTimeMs ?? 0,
    writable: true,
  });
  Object.defineProperty(element, "durationMs", {
    value: props.durationMs ?? 1000,
    writable: true,
  });
  Object.defineProperty(element, "startTimeMs", {
    value: props.startTimeMs ?? 0,
    writable: true,
  });
  Object.defineProperty(element, "endTimeMs", {
    value: props.endTimeMs ?? 1000,
    writable: true,
  });
  Object.defineProperty(element, "rootTimegroup", {
    value: props.rootTimegroup,
    writable: true,
  });
  Object.defineProperty(element, "parentTimegroup", {
    value: props.parentTimegroup,
    writable: true,
  });
  document.body.appendChild(element);
  return element;
}

describe("Timeline Element Synchronizer", () => {
  describe("CSS custom properties", () => {
    test("sets --ef-progress based on currentTimeMs/durationMs ratio", () => {
      const element = createTestElement({
        currentTimeMs: 250,
        durationMs: 1000,
      });

      updateAnimations(element);

      assert.equal(element.style.getPropertyValue("--ef-progress"), "25%");
    });

    test("clamps --ef-progress to 0-100% range", () => {
      const element1 = createTestElement({
        currentTimeMs: -100,
        durationMs: 1000,
      });
      const element2 = createTestElement({
        currentTimeMs: 1500,
        durationMs: 1000,
      });

      updateAnimations(element1);
      updateAnimations(element2);

      assert.equal(element1.style.getPropertyValue("--ef-progress"), "0%");
      assert.equal(element2.style.getPropertyValue("--ef-progress"), "100%");
    });

    test("sets --ef-duration to element durationMs", () => {
      const element = createTestElement({
        durationMs: 2000,
      });

      updateAnimations(element);

      assert.equal(element.style.getPropertyValue("--ef-duration"), "2000ms");
    });

    test("sets --ef-transition-duration based on parentTimegroup overlapMs", () => {
      const parentTimegroup = document.createElement(
        "ef-timegroup",
      ) as EFTimegroup;
      parentTimegroup.overlapMs = 500;

      const element = createTestElement({
        parentTimegroup,
      });

      updateAnimations(element);

      assert.equal(
        element.style.getPropertyValue("--ef-transition-duration"),
        "500ms",
      );
    });

    test("sets --ef-transition-duration to 0ms when no parentTimegroup", () => {
      const element = createTestElement();

      updateAnimations(element);

      assert.equal(
        element.style.getPropertyValue("--ef-transition-duration"),
        "0ms",
      );
    });

    test("sets --ef-transition-out-start correctly", () => {
      const parentTimegroup = document.createElement(
        "ef-timegroup",
      ) as EFTimegroup;
      parentTimegroup.overlapMs = 200;

      const element = createTestElement({
        durationMs: 1000,
        parentTimegroup,
      });

      updateAnimations(element);

      assert.equal(
        element.style.getPropertyValue("--ef-transition-out-start"),
        "800ms",
      );
    });

    test("sets animation-related CSS properties when animations are present", () => {
      const element = createTestElement({
        durationMs: 2000,
      });

      // Create an animation to trigger CSS property setting
      element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 });

      updateAnimations(element);

      assert.equal(element.style.getPropertyValue("--ef-duration"), "2000ms");
      assert.equal(
        element.style.getPropertyValue("--ef-transition-duration"),
        "0ms",
      );
      assert.equal(
        element.style.getPropertyValue("--ef-transition-out-start"),
        "2000ms",
      );
    });
  });

  describe("element visibility", () => {
    test("hides element when timeline is before startTimeMs", () => {
      const rootTimegroup = document.createElement(
        "ef-timegroup",
      ) as EFTimegroup;
      rootTimegroup.currentTimeMs = 100;

      const element = createTestElement({
        startTimeMs: 200,
        endTimeMs: 800,
        rootTimegroup,
      });

      updateAnimations(element);

      assert.equal(element.style.display, "none");
    });

    test("hides element when timeline is after endTimeMs", () => {
      const rootTimegroup = document.createElement(
        "ef-timegroup",
      ) as EFTimegroup;
      rootTimegroup.currentTimeMs = 900;

      const element = createTestElement({
        startTimeMs: 200,
        endTimeMs: 800,
        rootTimegroup,
      });

      updateAnimations(element);

      assert.equal(element.style.display, "none");
    });

    test("shows element when timeline is within start/end range (using element currentTimeMs)", () => {
      const element = createTestElement({
        currentTimeMs: 500,
        startTimeMs: 200,
        endTimeMs: 800,
      });
      // Start with element hidden
      element.style.display = "none";

      updateAnimations(element);

      assert.equal(element.style.display, "");
    });

    test("uses element currentTimeMs when no rootTimegroup", () => {
      const element = createTestElement({
        currentTimeMs: 500,
        startTimeMs: 200,
        endTimeMs: 800,
      });
      element.style.display = "none";

      updateAnimations(element);

      assert.equal(element.style.display, "");
    });

    test("element at exact start boundary is visible (using element currentTimeMs)", () => {
      const element = createTestElement({
        currentTimeMs: 200,
        startTimeMs: 200,
        endTimeMs: 800,
      });
      element.style.display = "none";

      updateAnimations(element);

      assert.equal(element.style.display, "");
    });

    test("element at exact end boundary is hidden (using element currentTimeMs)", () => {
      const element = createTestElement({
        currentTimeMs: 800,
        startTimeMs: 200,
        endTimeMs: 800,
      });
      element.style.display = "";

      updateAnimations(element);

      assert.equal(element.style.display, "none");
    });

    test("element just before start boundary is hidden", () => {
      const element = createTestElement({
        currentTimeMs: 199,
        startTimeMs: 200,
        endTimeMs: 800,
      });

      updateAnimations(element);

      assert.equal(element.style.display, "none");
    });

    test("element just after end boundary is hidden", () => {
      const element = createTestElement({
        currentTimeMs: 801,
        startTimeMs: 200,
        endTimeMs: 800,
      });

      updateAnimations(element);

      assert.equal(element.style.display, "none");
    });
  });

  describe("Web Animations API integration", () => {
    test("skips animation processing when getAnimations is not available", () => {
      const element = createTestElement();
      // Mock missing getAnimations
      delete (element as any).getAnimations;

      // Should not throw and should still set CSS properties
      updateAnimations(element);

      assert.equal(element.style.getPropertyValue("--ef-progress"), "0%");
    });

    test("pauses running animations", async () => {
      const element = createTestElement();

      // Create a test animation
      const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: 1000,
      });
      animation.play();

      assert.equal(animation.playState, "running");

      updateAnimations(element);

      assert.equal(animation.playState, "paused");
    });

    test("ignores animations without KeyframeEffect", async () => {
      const element = createTestElement();

      // Create animation with non-KeyframeEffect (this is tricky to test, but we can verify no errors)
      const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: 1000,
      });

      // Mock the effect to not be a KeyframeEffect
      Object.defineProperty(animation, "effect", {
        value: {},
        writable: false,
      });

      // Should not throw
      updateAnimations(element);
    });

    test("ignores animations without target", async () => {
      const element = createTestElement();

      const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: 1000,
      });

      // Mock the effect target to be null
      if (animation.effect instanceof KeyframeEffect) {
        Object.defineProperty(animation.effect, "target", {
          value: null,
          writable: false,
        });
      }

      // Should not throw
      updateAnimations(element);
    });

    test("handles missing timeTarget gracefully", async () => {
      const element = createTestElement();

      const target = document.createElement("div");
      element.appendChild(target);

      // Should not throw when target has no temporal parent
      updateAnimations(element);
    });

    test("processes multiple animations on same element", async () => {
      const element = createTestElement();

      const animation1 = element.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: 1000,
      });
      const animation2 = element.animate(
        [{ transform: "scale(1)" }, { transform: "scale(2)" }],
        { duration: 500 },
      );

      animation1.play();
      animation2.play();

      assert.equal(animation1.playState, "running");
      assert.equal(animation2.playState, "running");

      updateAnimations(element);

      // Both animations should be paused
      assert.equal(animation1.playState, "paused");
      assert.equal(animation2.playState, "paused");
    });

    test("handles animations with zero duration", async () => {
      const element = createTestElement();

      // Should not throw
      updateAnimations(element);
    });

    test("handles animations with zero iterations", async () => {
      const element = createTestElement();

      const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: 1000,
        iterations: 0,
      });

      updateAnimations(element);

      // With 0 iterations and no timeTarget, currentIteration (0) >= iterations (0), so should be set to duration - epsilon
      // But since there's no timeTarget, the code path continues and doesn't set currentTime
      // The animation will continue with its default behavior
      assert.equal(animation.currentTime, 0);
    });

    test("handles animations that are already paused", async () => {
      const element = createTestElement();

      const animation = element.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: 1000,
      });
      animation.pause();

      assert.equal(animation.playState, "paused");

      // Should not throw and should still set currentTime
      updateAnimations(element);

      assert.equal(animation.playState, "paused");
    });
  });

  describe("edge cases", () => {
    test("handles zero duration gracefully", () => {
      const element = createTestElement({
        currentTimeMs: 100,
        durationMs: 0,
      });

      updateAnimations(element);

      // Should handle division by zero
      assert.equal(element.style.getPropertyValue("--ef-progress"), "100%");
    });

    test("handles negative currentTimeMs", () => {
      const element = createTestElement({
        currentTimeMs: -100,
        durationMs: 1000,
      });

      updateAnimations(element);

      assert.equal(element.style.getPropertyValue("--ef-progress"), "0%");
    });

    test("handles missing parentTimegroup overlapMs", () => {
      const parentTimegroup = {} as EFTimegroup; // Missing overlapMs property

      const element = createTestElement({
        parentTimegroup,
        durationMs: 1000,
      });

      updateAnimations(element);

      assert.equal(
        element.style.getPropertyValue("--ef-transition-duration"),
        "0ms",
      );
      assert.equal(
        element.style.getPropertyValue("--ef-transition-out-start"),
        "1000ms",
      );
    });

    test("handles large duration values", () => {
      const element = createTestElement({
        currentTimeMs: 5000,
        durationMs: 1000000, // 1000 seconds
      });

      updateAnimations(element);

      assert.equal(element.style.getPropertyValue("--ef-progress"), "0.5%");
    });

    test("handles very small time values", () => {
      const element = createTestElement({
        currentTimeMs: 0.5,
        durationMs: 10,
      });

      updateAnimations(element);

      assert.equal(element.style.getPropertyValue("--ef-progress"), "5%");
    });

    test("does not modify display when element should remain visible", () => {
      const element = createTestElement({
        currentTimeMs: 500,
        startTimeMs: 200,
        endTimeMs: 800,
      });
      // Element starts visible (default state)
      const initialDisplay = element.style.display;

      updateAnimations(element);

      // Should not have been modified
      assert.equal(element.style.display, initialDisplay);
    });

    test("does not modify display when element should remain hidden", () => {
      const element = createTestElement({
        currentTimeMs: 100,
        startTimeMs: 200,
        endTimeMs: 800,
      });
      element.style.display = "none";

      updateAnimations(element);

      // Should still be hidden
      assert.equal(element.style.display, "none");
    });
  });

  describe("CSS variables with zero overlap", () => {
    test("correctly sets transition duration to 0ms when overlap is 0ms", () => {
      const element = createTestElement({
        currentTimeMs: 500,
        startTimeMs: 0,
        endTimeMs: 1000,
        durationMs: 1000,
        parentTimegroup: {
          overlapMs: 0, // Zero overlap case
        } as any,
      });

      updateAnimations(element);

      // When overlap is 0, transition duration should be 0ms (no overlap means no transition)
      const transitionDuration = element.style.getPropertyValue(
        "--ef-transition-duration",
      );
      assert.equal(
        transitionDuration,
        "0ms",
        "Transition duration should be 0ms when no overlap",
      );

      // Transition out start should equal full duration (no transition period)
      const transitionOutStart = element.style.getPropertyValue(
        "--ef-transition-out-start",
      );
      assert.equal(
        transitionOutStart,
        "1000ms",
        "Transition out start should equal full duration when no overlap",
      );

      // Duration variable should still be set correctly for within-clip animations
      const duration = element.style.getPropertyValue("--ef-duration");
      assert.equal(
        duration,
        "1000ms",
        "Duration should be set for within-clip animation calculations",
      );
    });
  });
});
