import { describe, expect, it, vi } from "vitest";
import { render, screen } from "~/test-utils.js";
import { FadeIn } from "./FadeIn.js";

// ---------------------------------------------------------------------------
// framer-motion mock
// ---------------------------------------------------------------------------
// Replace motion.div with a plain <div> that forwards all non-animation props
// plus exposes animation-variant props as data-* attributes for assertions.

vi.mock("framer-motion", () => ({
  motion: {
    div: ({
      children,
      className,
      initial,
      whileInView,
      viewport,
      variants,
      ...rest
    }: React.HTMLAttributes<HTMLDivElement> & {
      children?: React.ReactNode;
      initial?: unknown;
      whileInView?: unknown;
      viewport?: unknown;
      variants?: unknown;
    }) => (
      <div
        className={className}
        data-initial={JSON.stringify(initial)}
        data-while-in-view={JSON.stringify(whileInView)}
        data-viewport={JSON.stringify(viewport)}
        data-variants={JSON.stringify(variants)}
        {...rest}
      >
        {children}
      </div>
    ),
  },
}));

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function getWrapper(container: HTMLElement) {
  return container.firstChild as HTMLElement;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("FadeIn", () => {
  // -------------------------------------------------------------------------
  // Children rendering
  // -------------------------------------------------------------------------

  describe("children rendering", () => {
    it("renders its children", () => {
      render(
        <FadeIn>
          <p>Hello FadeIn</p>
        </FadeIn>,
      );
      expect(screen.getByText("Hello FadeIn")).toBeInTheDocument();
    });

    it("renders multiple children", () => {
      render(
        <FadeIn>
          <span>First</span>
          <span>Second</span>
        </FadeIn>,
      );
      expect(screen.getByText("First")).toBeInTheDocument();
      expect(screen.getByText("Second")).toBeInTheDocument();
    });

    it("renders text node children", () => {
      render(<FadeIn>plain text</FadeIn>);
      expect(screen.getByText("plain text")).toBeInTheDocument();
    });
  });

  // -------------------------------------------------------------------------
  // className prop
  // -------------------------------------------------------------------------

  describe("className prop", () => {
    it("applies className to the wrapper div", () => {
      const { container } = render(<FadeIn className="custom-class">content</FadeIn>);
      expect(getWrapper(container)).toHaveClass("custom-class");
    });

    it("does not apply a className when the prop is omitted", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      // className attribute should be absent or empty
      const wrapper = getWrapper(container);
      expect(wrapper.className).toBeFalsy();
    });
  });

  // -------------------------------------------------------------------------
  // Animation props (initial / whileInView)
  // -------------------------------------------------------------------------

  describe("animation configuration", () => {
    it("sets initial to 'hidden'", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      const wrapper = getWrapper(container);
      expect(wrapper.dataset["initial"]).toBe(JSON.stringify("hidden"));
    });

    it("sets whileInView to 'visible'", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      const wrapper = getWrapper(container);
      expect(wrapper.dataset["whileInView"]).toBe(JSON.stringify("visible"));
    });

    it("passes once=true to the viewport by default", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      const viewport = JSON.parse(getWrapper(container).dataset["viewport"]!) as {
        once: boolean;
        margin: string;
      };
      expect(viewport.once).toBe(true);
    });

    it("passes once=false to the viewport when once={false}", () => {
      const { container } = render(<FadeIn once={false}>content</FadeIn>);
      const viewport = JSON.parse(getWrapper(container).dataset["viewport"]!) as {
        once: boolean;
      };
      expect(viewport.once).toBe(false);
    });

    it("passes margin='-40px' to the viewport", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      const viewport = JSON.parse(getWrapper(container).dataset["viewport"]!) as {
        margin: string;
      };
      expect(viewport.margin).toBe("-40px");
    });
  });

  // -------------------------------------------------------------------------
  // Variants — direction offsets
  // -------------------------------------------------------------------------

  describe("direction offsets in variants", () => {
    function getVariants(container: HTMLElement) {
      return JSON.parse(getWrapper(container).dataset["variants"]!) as {
        hidden: { opacity: number; x: number; y: number };
        visible: { opacity: number; x: number; y: number; transition: object };
      };
    }

    it("direction='up' (default) sets positive y offset in hidden state", () => {
      const { container } = render(<FadeIn distance={12}>content</FadeIn>);
      const { hidden } = getVariants(container);
      expect(hidden.y).toBe(12);
      expect(hidden.x).toBe(0);
      expect(hidden.opacity).toBe(0);
    });

    it("direction='down' sets negative y offset in hidden state", () => {
      const { container } = render(
        <FadeIn direction="down" distance={12}>
          content
        </FadeIn>,
      );
      const { hidden } = getVariants(container);
      expect(hidden.y).toBe(-12);
      expect(hidden.x).toBe(0);
    });

    it("direction='left' sets positive x offset in hidden state", () => {
      const { container } = render(
        <FadeIn direction="left" distance={20}>
          content
        </FadeIn>,
      );
      const { hidden } = getVariants(container);
      expect(hidden.x).toBe(20);
      expect(hidden.y).toBe(0);
    });

    it("direction='right' sets negative x offset in hidden state", () => {
      const { container } = render(
        <FadeIn direction="right" distance={20}>
          content
        </FadeIn>,
      );
      const { hidden } = getVariants(container);
      expect(hidden.x).toBe(-20);
      expect(hidden.y).toBe(0);
    });

    it("direction='none' sets both x and y offsets to 0 in hidden state", () => {
      const { container } = render(
        <FadeIn direction="none" distance={20}>
          content
        </FadeIn>,
      );
      const { hidden } = getVariants(container);
      expect(hidden.x).toBe(0);
      expect(hidden.y).toBe(0);
    });

    it("visible state always has opacity=1, x=0, y=0", () => {
      const directions = ["up", "down", "left", "right", "none"] as const;
      for (const direction of directions) {
        const { container } = render(
          <FadeIn direction={direction} distance={16}>
            content
          </FadeIn>,
        );
        const { visible } = getVariants(container);
        expect(visible.opacity).toBe(1);
        expect(visible.x).toBe(0);
        expect(visible.y).toBe(0);
      }
    });
  });

  // -------------------------------------------------------------------------
  // Variants — transition options
  // -------------------------------------------------------------------------

  describe("transition options in variants", () => {
    function getTransition(container: HTMLElement) {
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        visible: { transition: { duration: number; delay: number; ease: number[] } };
      };
      return variants.visible.transition;
    }

    it("uses the default duration of 0.3 when not specified", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      expect(getTransition(container).duration).toBe(0.3);
    });

    it("uses the provided duration prop", () => {
      const { container } = render(<FadeIn duration={0.6}>content</FadeIn>);
      expect(getTransition(container).duration).toBe(0.6);
    });

    it("uses the default delay of 0 when not specified", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      expect(getTransition(container).delay).toBe(0);
    });

    it("uses the provided delay prop", () => {
      const { container } = render(<FadeIn delay={0.2}>content</FadeIn>);
      expect(getTransition(container).delay).toBe(0.2);
    });

    it("uses the correct cubic-bezier ease array", () => {
      const { container } = render(<FadeIn>content</FadeIn>);
      expect(getTransition(container).ease).toEqual([0.25, 0.1, 0.25, 1]);
    });
  });

  // -------------------------------------------------------------------------
  // Distance prop
  // -------------------------------------------------------------------------

  describe("distance prop", () => {
    it("defaults to a distance of 12", () => {
      const { container } = render(<FadeIn direction="up">content</FadeIn>);
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        hidden: { y: number };
      };
      expect(variants.hidden.y).toBe(12);
    });

    it("uses a custom distance value", () => {
      const { container } = render(
        <FadeIn direction="up" distance={32}>
          content
        </FadeIn>,
      );
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        hidden: { y: number };
      };
      expect(variants.hidden.y).toBe(32);
    });
  });
});
