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

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

/** Returns the single shimmer div rendered by ShimmerSkeleton. */
function getSkeleton(container: HTMLElement) {
  return container.firstChild as HTMLElement;
}

// ---------------------------------------------------------------------------
// ShimmerSkeleton
// ---------------------------------------------------------------------------

describe("ShimmerSkeleton", () => {
  describe("variant defaults", () => {
    it("applies the shimmer-skeleton base class", () => {
      const { container } = render(<ShimmerSkeleton />);
      expect(getSkeleton(container)).toHaveClass("shimmer-skeleton");
    });

    it("defaults to the 'rect' variant: 100% wide, 40px tall, rounded-md", () => {
      const { container } = render(<ShimmerSkeleton />);
      const el = getSkeleton(container);
      expect(el.style.width).toBe("100%");
      expect(el.style.height).toBe("40px");
      expect(el).toHaveClass("rounded-md");
    });

    it("'text' variant uses 100% width, 1em height, rounded", () => {
      const { container } = render(<ShimmerSkeleton variant="text" />);
      const el = getSkeleton(container);
      expect(el.style.width).toBe("100%");
      expect(el.style.height).toBe("1em");
      expect(el).toHaveClass("rounded");
    });

    it("'circle' variant uses 40px width, 40px height, rounded-full", () => {
      const { container } = render(<ShimmerSkeleton variant="circle" />);
      const el = getSkeleton(container);
      expect(el.style.width).toBe("40px");
      expect(el.style.height).toBe("40px");
      expect(el).toHaveClass("rounded-full");
    });

    it("'card' variant uses 100% width, 120px height, rounded-xl", () => {
      const { container } = render(<ShimmerSkeleton variant="card" />);
      const el = getSkeleton(container);
      expect(el.style.width).toBe("100%");
      expect(el.style.height).toBe("120px");
      expect(el).toHaveClass("rounded-xl");
    });
  });

  describe("explicit width / height", () => {
    it("accepts a numeric width and converts it to px", () => {
      const { container } = render(<ShimmerSkeleton width={200} />);
      expect(getSkeleton(container).style.width).toBe("200px");
    });

    it("accepts a string width directly", () => {
      const { container } = render(<ShimmerSkeleton width="50%" />);
      expect(getSkeleton(container).style.width).toBe("50%");
    });

    it("accepts a numeric height and converts it to px", () => {
      const { container } = render(<ShimmerSkeleton height={80} />);
      expect(getSkeleton(container).style.height).toBe("80px");
    });

    it("accepts a string height directly", () => {
      const { container } = render(<ShimmerSkeleton height="3rem" />);
      expect(getSkeleton(container).style.height).toBe("3rem");
    });

    it("explicit width overrides the variant default", () => {
      const { container } = render(<ShimmerSkeleton variant="circle" width={64} />);
      expect(getSkeleton(container).style.width).toBe("64px");
    });

    it("explicit height overrides the variant default", () => {
      const { container } = render(<ShimmerSkeleton variant="card" height={200} />);
      expect(getSkeleton(container).style.height).toBe("200px");
    });
  });

  describe("rounded prop", () => {
    const roundedCases: Array<
      [NonNullable<React.ComponentProps<typeof ShimmerSkeleton>["rounded"]>, string]
    > = [
      ["sm", "rounded-sm"],
      ["md", "rounded-md"],
      ["lg", "rounded-lg"],
      ["xl", "rounded-xl"],
      ["2xl", "rounded-2xl"],
      ["full", "rounded-full"],
    ];

    for (const [rounded, expectedClass] of roundedCases) {
      it(`rounded="${rounded}" applies class ${expectedClass}`, () => {
        const { container } = render(<ShimmerSkeleton rounded={rounded} />);
        expect(getSkeleton(container)).toHaveClass(expectedClass);
      });
    }

    it("explicit rounded prop overrides the variant default", () => {
      // 'circle' default is rounded-full; override with 'md'
      const { container } = render(<ShimmerSkeleton variant="circle" rounded="md" />);
      const el = getSkeleton(container);
      expect(el).toHaveClass("rounded-md");
      expect(el).not.toHaveClass("rounded-full");
    });
  });

  describe("className prop", () => {
    it("appends extra className to the element", () => {
      const { container } = render(<ShimmerSkeleton className="mt-4 w-1/2" />);
      const el = getSkeleton(container);
      expect(el).toHaveClass("mt-4");
      expect(el).toHaveClass("w-1/2");
    });

    it("preserves the shimmer-skeleton base class when className is provided", () => {
      const { container } = render(<ShimmerSkeleton className="extra" />);
      expect(getSkeleton(container)).toHaveClass("shimmer-skeleton");
    });
  });
});

// ---------------------------------------------------------------------------
// ShimmerGroup
// ---------------------------------------------------------------------------

/** Returns all shimmer divs that are direct children of the ShimmerGroup wrapper. */
function getGroupChildren(container: HTMLElement): HTMLElement[] {
  // ShimmerGroup renders a single wrapper div; its children are the skeleton divs.
  const wrapper = container.firstChild as HTMLElement;
  return Array.from(wrapper.children) as HTMLElement[];
}

describe("ShimmerGroup", () => {
  it("renders all children", () => {
    const { container } = render(
      <ShimmerGroup>
        <ShimmerSkeleton />
        <ShimmerSkeleton />
        <ShimmerSkeleton />
      </ShimmerGroup>,
    );
    expect(getGroupChildren(container)).toHaveLength(3);
  });

  it("injects animationDelay on each valid React child", () => {
    // ShimmerGroup uses cloneElement to inject style.animationDelay.
    // Use plain divs so the style prop is forwarded to the DOM.
    const { container } = render(
      <ShimmerGroup staggerMs={100}>
        <div className="shimmer-skeleton" />
        <div className="shimmer-skeleton" />
        <div className="shimmer-skeleton" />
      </ShimmerGroup>,
    );
    const children = getGroupChildren(container);
    expect(["", "0ms"]).toContain(children[0]!.style.animationDelay);
    expect(children[1]!.style.animationDelay).toBe("100ms");
    expect(children[2]!.style.animationDelay).toBe("200ms");
  });

  it("uses 50ms as the default stagger delay", () => {
    const { container } = render(
      <ShimmerGroup>
        <div className="shimmer-skeleton" />
        <div className="shimmer-skeleton" />
      </ShimmerGroup>,
    );
    const children = getGroupChildren(container);
    expect(["", "0ms"]).toContain(children[0]!.style.animationDelay);
    expect(children[1]!.style.animationDelay).toBe("50ms");
  });

  it("applies a custom className to the wrapper div", () => {
    const { container } = render(
      <ShimmerGroup className="grid grid-cols-3">
        <ShimmerSkeleton />
      </ShimmerGroup>,
    );
    expect(container.firstChild).toHaveClass("grid");
    expect(container.firstChild).toHaveClass("grid-cols-3");
  });

  it("passes through non-element children without crashing", () => {
    // A text node is not a valid React element and should be returned as-is.
    const { container } = render(
      <ShimmerGroup>
        {"plain text"}
        <ShimmerSkeleton />
      </ShimmerGroup>,
    );
    expect(container).toHaveTextContent("plain text");
    // One skeleton element should still be present.
    expect(container.querySelectorAll(".shimmer-skeleton")).toHaveLength(1);
  });

  it("preserves existing style properties when merged with animationDelay", () => {
    // Use plain divs with explicit style so cloneElement merges correctly.
    const { container } = render(
      <ShimmerGroup staggerMs={75}>
        <div style={{ width: "120px", height: "30px" }} />
        <div style={{ width: "120px", height: "30px" }} />
      </ShimmerGroup>,
    );
    const children = getGroupChildren(container);
    expect(children[1]!.style.animationDelay).toBe("75ms");
    expect(children[0]!.style.width).toBe("120px");
    expect(children[0]!.style.height).toBe("30px");
    expect(children[1]!.style.width).toBe("120px");
    expect(children[1]!.style.height).toBe("30px");
  });
});

// ---------------------------------------------------------------------------
// ShimmerStyles
// ---------------------------------------------------------------------------

describe("ShimmerStyles", () => {
  it("renders a <style> element", () => {
    const { container } = render(<ShimmerStyles />);
    const styleEl = container.querySelector("style");
    expect(styleEl).not.toBeNull();
  });

  it("style element contains the shimmer keyframe definition", () => {
    const { container } = render(<ShimmerStyles />);
    const styleEl = container.querySelector("style");
    expect(styleEl?.innerHTML).toContain("shimmer");
  });

  it("style element contains the shimmer-skeleton class definition", () => {
    const { container } = render(<ShimmerStyles />);
    const styleEl = container.querySelector("style");
    expect(styleEl?.innerHTML).toContain(".shimmer-skeleton");
  });
});
