import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { DefinitionEngine, hasPotentialDefinitionForUrl, matchesUrlPattern, selectDefinition, selectDefinitionForUrl } from "../src/content/definition-engine";
import { BUILTIN_DEFINITIONS } from "../src/content/definitions";
import type { SiteDefinition } from "../src/shared/types";

const definition: SiteDefinition = {
  id: "example-social",
  name: "Example Social",
  urlPatterns: ["https://*.example.test/*"],
  requiredSelectors: [".feed"],
  post: { rootSelectors: ["article.post"], textSelectors: [".content"], excludedSelectors: [".exclude"], idAttributes: ["data-id"], permalinkSelectors: ["a.permalink[href]"] }
};

describe("DefinitionEngine", () => {
  it("discovers the supplied root, extracts only visible text, and uses a stable ID", () => {
    document.body.innerHTML = '<article class="post" data-id="42"><div class="content">Visible <span class="exclude">skip</span><span style="display:none">hidden</span></div></article>';
    const post = new DefinitionEngine(definition).discover(document.querySelector("article")!)[0];
    expect(post).toMatchObject({ id: "example-social:42", text: "visible" });
  });

  it("uses permalink IDs and selects only definitions with URL and marker matches", () => {
    document.body.innerHTML = '<div class="feed"><article class="post"><div class="content">Hello</div><a class="permalink" href="/users/a/statuses/1">link</a></article></div>';
    const engine = new DefinitionEngine(definition);
    expect(engine.discover(document)[0]?.id).toContain("/users/a/statuses/1");
    expect(selectDefinition([definition], "https://social.example.test/home")).toBe(definition);
    expect(selectDefinition([definition], "https://other.test/home")).toBeUndefined();
  });

  it("matches browser-style URL patterns", () => {
    expect(matchesUrlPattern("https://*.example.test/*", "https://a.example.test/path?q=1")).toBe(true);
    expect(matchesUrlPattern("https://*.example.test/*", "http://a.example.test/path")).toBe(false);
  });

  it("covers Mastodon timeline, profile, and detailed reply status containers", () => {
    document.body.innerHTML = `
      <div class="status" data-id="timeline"><div class="status__content">Timeline post</div></div>
      <div class="status" data-id="profile"><div class="status__content">Profile post</div></div>
      <div class="detailed-status"><a class="detailed-status__datetime" href="/users/a/statuses/3"></a><div class="status__content">Reply in thread</div></div>`;
    const posts = new DefinitionEngine(BUILTIN_DEFINITIONS[0]!).discover(document);
    expect(posts.map((post) => post.id)).toEqual(["mastodon:timeline", "mastodon:profile", "mastodon:http://localhost:3000/users/a/statuses/3"]);
    expect(posts.map((post) => post.text)).toEqual(["timeline post", "profile post", "reply in thread"]);
  });

  it("does not include hidden Mastodon content-warning text until it is revealed", () => {
    document.body.innerHTML = '<div class="status" data-id="cw"><div class="status__content"><p>Content warning <a class="status__content__spoiler-link">Show more</a></p><div id="hidden" style="display:none">Hidden post text</div></div></div>';
    const engine = new DefinitionEngine(BUILTIN_DEFINITIONS[0]!);
    expect(engine.discover(document)[0]?.text).toBe("content warning");
    document.querySelector("#hidden")?.removeAttribute("style");
    expect(engine.discover(document)[0]?.text).toBe("content warning hidden post text");
  });

  it("rediscovers a Mastodon status when a content warning is revealed", async () => {
    document.body.innerHTML = '<div class="status" data-id="cw"><div class="status__content"><p>Content warning</p><div id="hidden" style="display:none">Revealed text</div></div></div>';
    const engine = new DefinitionEngine(BUILTIN_DEFINITIONS[0]!);
    const observed: string[] = [];
    const stop = engine.observe((posts) => observed.push(...posts.map((post) => post.text)));
    document.querySelector("#hidden")?.removeAttribute("style");
    await new Promise((resolve) => setTimeout(resolve, 0));
    stop();
    expect(observed).toContain("content warning revealed text");
  });

  it("discovers Threads posts appended after observation starts", async () => {
    document.body.innerHTML = '<main><div role="article"><span dir="auto">Initial post</span></div></main>';
    const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!;
    const engine = new DefinitionEngine(threads);
    const observed: string[] = [];
    const stop = engine.observe((posts) => observed.push(...posts.map((post) => post.text)));
    const post = document.createElement("div");
    post.setAttribute("role", "article");
    post.innerHTML = '<span dir="auto">Scrolled-in post</span>';
    document.querySelector("main")?.append(post);
    await new Promise((resolve) => setTimeout(resolve, 0));
    stop();
    expect(observed).toContain("scrolled-in post");
  });

  it("discovers Threads posts from stable semantic/data selectors and permalink IDs", () => {
    document.body.innerHTML = readFileSync("tests/fixtures/threads-feed.html", "utf8");
    const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!;
    const posts = new DefinitionEngine(threads).discover(document);
    expect(posts.map((post) => post.id)).toEqual(["threads:http://localhost:3000/@alice/post/ABC123", "threads:http://localhost:3000/@bob/post/DEF456"]);
    expect(posts.map((post) => post.text)).toEqual(["first threads post with visible text", "a reply in the conversation"]);
  });

  it("discovers Threads detail-page posts from pagelet containers", () => {
    document.body.innerHTML = readFileSync("tests/fixtures/threads-post-page.html", "utf8");
    const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!;
    const posts = new DefinitionEngine(threads).discover(document);
    expect(posts.map((post) => post.id)).toEqual([
      "threads:http://localhost:3000/@alice/post/ABC123",
      "threads:http://localhost:3000/@bob/post/DEF456"
    ]);
    expect(posts.map((post) => post.text)).toEqual(["the main post on the threads detail page", "a reply on the threads detail page"]);
  });

  it("selects Threads on current and legacy domains", () => {
    const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!;
    document.body.innerHTML = '<div data-pressable-container="true"></div>';
    for (const url of ["https://threads.com/home", "https://www.threads.com/@alice/post/ABC123", "https://threads.net/home", "https://www.threads.net/@alice/post/ABC123"]) {
      expect(selectDefinition([threads], url)).toBe(threads);
    }
    expect(selectDefinition([threads], "https://example.com/home")).toBeUndefined();
  });

  it("can identify a specific site while its SPA markers are still loading", () => {
    const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!;
    document.body.innerHTML = "";
    expect(selectDefinition([threads], "https://www.threads.com/")).toBeUndefined();
    expect(selectDefinitionForUrl([BUILTIN_DEFINITIONS[0]!, threads], "https://www.threads.com/")).toBe(threads);
  });

  it("keeps broad definitions pending until their hydration markers appear", () => {
    const mastodon = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "mastodon")!;
    const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!;
    document.body.innerHTML = "";
    expect(selectDefinition([mastodon], "https://mastodon.example/@user")).toBeUndefined();
    expect(selectDefinitionForUrl([mastodon], "https://mastodon.example/@user")).toBeUndefined();
    expect(hasPotentialDefinitionForUrl([mastodon], "https://mastodon.example/@user")).toBe(true);
    expect(hasPotentialDefinitionForUrl([threads], "https://example.com/")).toBe(false);
    document.body.innerHTML = '<div class="status"><div class="status__content">Ready</div></div>';
    expect(selectDefinition([mastodon], "https://mastodon.example/@user")).toBe(mastodon);
  });
});
