import { describe, expect, it } from "vitest";
import { isValidElement, type ReactNode } from "react";
import { linkify } from "./linkify";

// linkify returns React nodes without rendering, so we can inspect them structurally in node env:
// an <a> element carries { href, children:label }, everything else is text held in a Fragment.
function links(nodes: ReactNode[]): { href: string; label: string }[] {
  return nodes
    .filter(
      (n): n is React.ReactElement<{ href: string; children: string }> =>
        isValidElement(n) && (n as React.ReactElement).type === "a",
    )
    .map((n) => ({ href: n.props.href, label: n.props.children }));
}

describe("linkify", () => {
  it("leaves plain prose untouched (no anchors)", () => {
    expect(links(linkify("just some text, nothing to see"))).toEqual([]);
  });

  it("links an explicit http(s) URL to itself", () => {
    expect(links(linkify("see https://example.com/watch?v=1 now"))).toEqual([
      { href: "https://example.com/watch?v=1", label: "https://example.com/watch?v=1" },
    ]);
  });

  it("gives a www.* host an https:// href but keeps the bare label", () => {
    expect(links(linkify("at www.example.com/foo"))).toEqual([
      { href: "https://www.example.com/foo", label: "www.example.com/foo" },
    ]);
  });

  it("links a bare domain.tld/path", () => {
    expect(links(linkify("go to example.com/foo please"))).toEqual([
      { href: "https://example.com/foo", label: "example.com/foo" },
    ]);
  });

  it("does not mistake a version number for a bare URL", () => {
    // letters-only TLD + a required path keep 1.2.3 out (no path, numeric last segment).
    expect(links(linkify("upgraded to 1.2.3 today"))).toEqual([]);
  });

  it("links only the handle of a '<platform>: @handle' mention, keeping the label as text", () => {
    const out = linkify("follow x: @jane please");
    expect(links(out)).toEqual([{ href: "https://x.com/jane", label: "@jane" }]);
    // the "x: " prefix survives as text, not swallowed into the link.
    const text = out
      .filter((n) => typeof n === "string" || (isValidElement(n) && n.type !== "a"))
      .map((n) => (isValidElement(n) ? (n.props as { children?: string }).children : n))
      .join("");
    expect(text).toContain("x: ");
  });

  it("maps a platform alias to its canonical base URL", () => {
    expect(links(linkify("ig: @foo"))).toEqual([
      { href: "https://instagram.com/foo", label: "@foo" },
    ]);
  });

  it("leaves a bare @handle with no platform, and #hashtags, as plain text", () => {
    expect(links(linkify("hi @nobody and #hashtag"))).toEqual([]);
  });

  it("handles several links in one string in order", () => {
    const out = links(linkify("https://a.com/x then youtube: @chan then b.org/y"));
    expect(out.map((l) => l.href)).toEqual([
      "https://a.com/x",
      "https://www.youtube.com/@chan",
      "https://b.org/y",
    ]);
  });
});
