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

// ---------------------------------------------------------------------------
// framer-motion mock
// ---------------------------------------------------------------------------
// All motion.* components become plain HTML equivalents that forward className,
// children and expose animation props as data-* attributes so we can assert
// on variants / initial / animate values without running actual animations.

type MotionProps = React.HTMLAttributes<HTMLElement> & {
  children?: React.ReactNode;
  initial?: unknown;
  animate?: unknown;
  variants?: unknown;
};

function makeMotionComponent(Tag: "div" | "ul" | "ol" | "tbody") {
  return function MockMotion({
    children,
    className,
    initial,
    animate,
    variants,
    ...rest
  }: MotionProps) {
    const dataProps = {
      "data-initial": JSON.stringify(initial),
      "data-animate": JSON.stringify(animate),
      "data-variants": JSON.stringify(variants),
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const Element = Tag as any;
    return (
      <Element className={className} {...dataProps} {...rest}>
        {children}
      </Element>
    );
  };
}

vi.mock("framer-motion", () => ({
  motion: {
    div: makeMotionComponent("div"),
    ul: makeMotionComponent("ul"),
    ol: makeMotionComponent("ol"),
    tbody: makeMotionComponent("tbody"),
  },
}));

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

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

// ---------------------------------------------------------------------------
// StaggerList tests
// ---------------------------------------------------------------------------

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

  describe("children rendering", () => {
    it("renders its children", () => {
      render(
        <StaggerList>
          <div>Item A</div>
          <div>Item B</div>
        </StaggerList>,
      );
      expect(screen.getByText("Item A")).toBeInTheDocument();
      expect(screen.getByText("Item B")).toBeInTheDocument();
    });

    it("renders with no children without crashing", () => {
      const { container } = render(<StaggerList>{null}</StaggerList>);
      expect(getWrapper(container)).toBeTruthy();
    });
  });

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

  describe("className prop", () => {
    it("applies className to the container element", () => {
      const { container } = render(<StaggerList className="grid gap-2">content</StaggerList>);
      expect(getWrapper(container)).toHaveClass("grid");
      expect(getWrapper(container)).toHaveClass("gap-2");
    });

    it("does not crash when className is omitted", () => {
      const { container } = render(<StaggerList>content</StaggerList>);
      expect(getWrapper(container)).toBeTruthy();
    });
  });

  // -------------------------------------------------------------------------
  // as prop — rendered element tag
  // -------------------------------------------------------------------------

  describe("as prop", () => {
    it("defaults to a <div> element", () => {
      const { container } = render(<StaggerList>content</StaggerList>);
      expect(getWrapper(container).tagName.toLowerCase()).toBe("div");
    });

    it("renders a <ul> when as='ul'", () => {
      const { container } = render(<StaggerList as="ul">content</StaggerList>);
      expect(getWrapper(container).tagName.toLowerCase()).toBe("ul");
    });

    it("renders an <ol> when as='ol'", () => {
      const { container } = render(<StaggerList as="ol">content</StaggerList>);
      expect(getWrapper(container).tagName.toLowerCase()).toBe("ol");
    });

    it("renders a <tbody> when as='tbody'", () => {
      // tbody must be inside a table for valid HTML, but jsdom accepts it.
      const { container } = render(
        <table>
          <StaggerList as="tbody">
            <tr>
              <td>cell</td>
            </tr>
          </StaggerList>
        </table>,
      );
      expect(container.querySelector("tbody")).not.toBeNull();
    });
  });

  // -------------------------------------------------------------------------
  // Animation props
  // -------------------------------------------------------------------------

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

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

    it("variants hidden state is an empty object", () => {
      const { container } = render(<StaggerList>content</StaggerList>);
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        hidden: Record<string, unknown>;
      };
      expect(variants.hidden).toEqual({});
    });

    it("variants visible state has a staggerChildren transition", () => {
      const { container } = render(<StaggerList>content</StaggerList>);
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        visible: { transition: { staggerChildren: number } };
      };
      expect(typeof variants.visible.transition.staggerChildren).toBe("number");
    });

    it("uses the default staggerDelay of 0.03", () => {
      const { container } = render(<StaggerList>content</StaggerList>);
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        visible: { transition: { staggerChildren: number } };
      };
      expect(variants.visible.transition.staggerChildren).toBe(0.03);
    });

    it("uses a custom staggerDelay when provided", () => {
      const { container } = render(<StaggerList staggerDelay={0.1}>content</StaggerList>);
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        visible: { transition: { staggerChildren: number } };
      };
      expect(variants.visible.transition.staggerChildren).toBe(0.1);
    });

    it("staggerDelay=0 is passed through correctly", () => {
      const { container } = render(<StaggerList staggerDelay={0}>content</StaggerList>);
      const variants = JSON.parse(getWrapper(container).dataset["variants"]!) as {
        visible: { transition: { staggerChildren: number } };
      };
      expect(variants.visible.transition.staggerChildren).toBe(0);
    });
  });
});

// ---------------------------------------------------------------------------
// StaggerItem tests
// ---------------------------------------------------------------------------

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

  describe("children rendering", () => {
    it("renders its children", () => {
      render(
        <StaggerItem>
          <span>List item content</span>
        </StaggerItem>,
      );
      expect(screen.getByText("List item content")).toBeInTheDocument();
    });

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

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

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

    it("renders without a className when the prop is omitted", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      // Must not throw and wrapper must exist.
      expect(getWrapper(container)).toBeTruthy();
    });
  });

  // -------------------------------------------------------------------------
  // Always renders a <div>
  // -------------------------------------------------------------------------

  it("always renders a <div> element", () => {
    const { container } = render(<StaggerItem>content</StaggerItem>);
    expect(getWrapper(container).tagName.toLowerCase()).toBe("div");
  });

  // -------------------------------------------------------------------------
  // Animation props / variants
  // -------------------------------------------------------------------------

  describe("animation configuration", () => {
    function getVariants(container: HTMLElement) {
      return JSON.parse(getWrapper(container).dataset["variants"]!) as {
        hidden: { opacity: number; y: number };
        visible: { opacity: number; y: number; transition: { duration: number; ease: number[] } };
      };
    }

    it("hidden variant has opacity=0", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      expect(getVariants(container).hidden.opacity).toBe(0);
    });

    it("hidden variant has y=8 (slides up slightly)", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      expect(getVariants(container).hidden.y).toBe(8);
    });

    it("visible variant has opacity=1", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      expect(getVariants(container).visible.opacity).toBe(1);
    });

    it("visible variant has y=0", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      expect(getVariants(container).visible.y).toBe(0);
    });

    it("visible transition has duration 0.25", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      expect(getVariants(container).visible.transition.duration).toBe(0.25);
    });

    it("visible transition has the correct cubic-bezier ease array", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      expect(getVariants(container).visible.transition.ease).toEqual([0.25, 0.1, 0.25, 1]);
    });

    it("does not set an explicit initial or animate prop (inherits from parent)", () => {
      const { container } = render(<StaggerItem>content</StaggerItem>);
      // StaggerItem does not pass initial/animate — motion should inherit them.
      // The data attributes will be 'undefined' serialised as the string "undefined"
      // if not passed, or absent. Both mean not explicitly set.
      const wrapper = getWrapper(container);
      const initialAttr = wrapper.dataset["initial"];
      const animateAttr = wrapper.dataset["animate"];
      expect(initialAttr === undefined || initialAttr === "undefined").toBe(true);
      expect(animateAttr === undefined || animateAttr === "undefined").toBe(true);
    });
  });

  // -------------------------------------------------------------------------
  // Composition: StaggerList + StaggerItem
  // -------------------------------------------------------------------------

  describe("StaggerList + StaggerItem composition", () => {
    it("renders nested StaggerItems inside a StaggerList", () => {
      render(
        <StaggerList>
          <StaggerItem>Alpha</StaggerItem>
          <StaggerItem>Beta</StaggerItem>
          <StaggerItem>Gamma</StaggerItem>
        </StaggerList>,
      );
      expect(screen.getByText("Alpha")).toBeInTheDocument();
      expect(screen.getByText("Beta")).toBeInTheDocument();
      expect(screen.getByText("Gamma")).toBeInTheDocument();
    });

    it("each StaggerItem renders its own wrapper div", () => {
      const { container } = render(
        <StaggerList>
          <StaggerItem>One</StaggerItem>
          <StaggerItem>Two</StaggerItem>
        </StaggerList>,
      );
      // The StaggerList wrapper contains two StaggerItem divs.
      const items = container.querySelectorAll("div > div");
      expect(items.length).toBeGreaterThanOrEqual(2);
    });

    it("renders StaggerItems inside a <ul> StaggerList", () => {
      render(
        <StaggerList as="ul">
          <StaggerItem>Item 1</StaggerItem>
          <StaggerItem>Item 2</StaggerItem>
        </StaggerList>,
      );
      expect(screen.getByText("Item 1")).toBeInTheDocument();
      expect(screen.getByText("Item 2")).toBeInTheDocument();
    });
  });
});
