import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";

await import("./linkedin-feed-dom.js");

const FeedDom = globalThis.NexusLinkedInFeedDom;

function control({ ariaLabel = "", text = "", pressed = null } = {}) {
  return {
    innerText: text,
    textContent: text,
    getAttribute(name) {
      if (name === "aria-label") return ariaLabel || null;
      if (name === "aria-pressed") return pressed;
      return null;
    },
  };
}

test("likeState recognizes current LinkedIn labels and excludes unrelated controls", () => {
  assert.equal(FeedDom.likeState(control({ ariaLabel: "React Like" })), "unliked");
  assert.equal(FeedDom.likeState(control({ ariaLabel: "Like Ada's post" })), "unliked");
  assert.equal(FeedDom.likeState(control({ text: "Like" })), "unliked");
  assert.equal(FeedDom.likeState(control({ ariaLabel: "React Unlike" })), "liked");
  assert.equal(FeedDom.likeState(control({ ariaLabel: "Unreact Like" })), "liked");
  assert.equal(FeedDom.likeState(control({ ariaLabel: "Remove reaction" })), "liked");
  assert.equal(FeedDom.likeState(control({ ariaLabel: "React Like", pressed: "true" })), "liked");
  assert.equal(FeedDom.likeState(control({ ariaLabel: "12 reactions" })), null);
  assert.equal(FeedDom.likeState(control({ ariaLabel: "Comment" })), null);
  assert.equal(FeedDom.likeState(control({ ariaLabel: "Comment", pressed: "true" })), null);
});

test("reaction-menu chevrons are not Like controls", () => {
  const reactions = control({ ariaLabel: "Open reactions menu", pressed: "false" });
  reactions.querySelector = () => ({ id: "chevron-up-medium" });
  assert.equal(FeedDom.likeState(reactions), null);
});

test("extractPostUrn accepts data attributes and encoded permalink URLs", () => {
  const fromData = {
    parentElement: null,
    getAttribute(name) {
      return name === "data-id" ? "urn:li:activity:12345" : null;
    },
    querySelectorAll() {
      return [];
    },
  };
  assert.equal(FeedDom.extractPostUrn(fromData), "urn:li:activity:12345");

  const link = {
    getAttribute(name) {
      return name === "href" ? "/feed/update/urn%3Ali%3AugcPost%3A67890/" : null;
    },
  };
  const fromLink = {
    parentElement: null,
    getAttribute() {
      return null;
    },
    querySelectorAll() {
      return [link];
    },
  };
  assert.equal(FeedDom.extractPostUrn(fromLink), "urn:li:ugcPost:67890");
});

test("scan discovers a hashed-class post from its accessible Like control", () => {
  const like = control({ ariaLabel: "React Like" });
  const comment = control({ ariaLabel: "Comment" });
  const post = {
    tagName: "DIV",
    innerText: "Ada Lovelace Market making infrastructure for web3 builders. ".repeat(2),
    textContent: "",
    parentElement: null,
    getAttribute() {
      return null;
    },
    querySelector(selector) {
      return selector.includes("/in/") ? { href: "/in/ada/" } : null;
    },
    querySelectorAll(selector) {
      if (selector === "button") return [like, comment];
      return [];
    },
  };
  like.parentElement = post;
  like.closest = () => null;
  const root = {
    querySelectorAll(selector) {
      if (selector.includes("mainFeed")) return [];
      return [like, comment];
    },
  };

  const results = FeedDom.scan(root);
  assert.equal(results.length, 1);
  assert.equal(results[0].post, post);
  assert.equal(results[0].likeButton, like);
  assert.match(results[0].text, /Market making/);
});

test("scan retains a profile post whose current control says Unreact Like", () => {
  const liked = control({ ariaLabel: "Unreact Like", pressed: "true" });
  const comment = control({ ariaLabel: "Comment" });
  const post = {
    tagName: "DIV",
    innerText:
      "Sripriya Battu already-liked activity post with enough content to identify its card. ".repeat(
        2,
      ),
    textContent: "",
    parentElement: null,
    getAttribute() {
      return null;
    },
    querySelector(selector) {
      return selector.includes("/in/") ? { href: "/in/sripriya-battu/" } : null;
    },
    querySelectorAll(selector) {
      if (selector.includes("button")) return [liked, comment];
      return [];
    },
  };
  liked.parentElement = post;
  liked.closest = () => null;
  const root = {
    querySelectorAll(selector) {
      if (selector.includes("mainFeed") || selector.includes("chevron-up")) return [];
      return [liked, comment];
    },
  };

  const results = FeedDom.scan(root);
  assert.equal(results.length, 1);
  assert.equal(results[0].post, post);
  assert.equal(results[0].liked, true);
});

test("scan chooses the explicit Style-2 Like after the reactions-menu chevron", () => {
  const icon = {
    id: "chevron-up-medium",
    parentElement: null,
    getAttribute(name) {
      return name === "id" ? this.id : null;
    },
    closest() {
      return likeControl;
    },
  };
  const reactionMenu = control({ ariaLabel: "Open reactions menu", pressed: "false" });
  reactionMenu.querySelector = (selector) => (selector.includes("chevron-up") ? icon : null);
  const likeControl = control({ ariaLabel: "Like", pressed: "false" });
  const post = {
    tagName: "DIV",
    innerText: "Grace Hopper explains compiler infrastructure and web3 markets.",
    textContent: "",
    parentElement: null,
    getAttribute(name) {
      return name === "componentkey" ? "expandedurn:li:activity:98765FeedType" : null;
    },
    querySelector(selector) {
      return selector.includes("chevron-up") ? icon : null;
    },
    querySelectorAll(selector) {
      if (selector.includes("button")) return [reactionMenu, likeControl];
      return [];
    },
  };
  const root = {
    querySelectorAll(selector) {
      if (selector.includes("mainFeed")) return [post];
      if (selector.includes("button")) return [reactionMenu, likeControl];
      return [];
    },
  };

  const results = FeedDom.scan(root);
  assert.equal(results.length, 1);
  assert.equal(results[0].likeButton, likeControl);
  assert.equal(results[0].liked, false);
  assert.equal(results[0].urn, "urn:li:activity:98765");
});

test("scan keeps Style-2 posts whose Like exists only in the reactions portal", () => {
  const reactionMenu = control({ ariaLabel: "Open reactions menu", pressed: "false" });
  const icon = {
    id: "chevron-up-small",
    closest: () => reactionMenu,
    getAttribute: (name) => (name === "id" ? "chevron-up-small" : null),
  };
  reactionMenu.querySelector = () => icon;
  const post = {
    innerText: "My remote team works across three time zones.",
    textContent: "",
    getAttribute: () => null,
    querySelector: (selector) => (selector.includes("chevron-up") ? icon : null),
    querySelectorAll: (selector) => (selector.includes("button") ? [reactionMenu] : []),
  };
  const root = {
    querySelectorAll(selector) {
      if (selector.includes("mainFeed")) return [post];
      if (selector.includes("button")) return [reactionMenu];
      return [];
    },
  };

  const [candidate] = FeedDom.scan(root);
  assert.equal(candidate.post, post);
  assert.equal(candidate.likeButton, reactionMenu);
  assert.equal(candidate.reactionMenuButton, reactionMenu);
  assert.equal(candidate.liked, false);
  assert.match(candidate.text, /remote/);
});

test("scan discovers a profile-activity Style-2 card outside mainFeed", () => {
  const reactionMenu = control({ ariaLabel: "Open reactions menu", pressed: "false" });
  const post = {
    innerText: "Employee update about tokenized market infrastructure.",
    textContent: "",
    parentElement: null,
    getAttribute: () => null,
    querySelector: () => null,
    querySelectorAll: (selector) => (selector.includes("button") ? [reactionMenu] : []),
  };
  reactionMenu.closest = (selector) => (selector.includes("listitem") ? post : null);
  reactionMenu.querySelector = () => null;
  const icon = {
    parentElement: null,
    closest: () => reactionMenu,
  };
  const root = {
    querySelectorAll(selector) {
      if (selector.includes("mainFeed")) return [];
      if (selector.includes('main svg[id^="chevron-up"]')) return [icon];
      if (selector.includes("button")) return [reactionMenu];
      return [];
    },
  };

  const [candidate] = FeedDom.scan(root);
  assert.equal(candidate.post, post);
  assert.equal(candidate.reactionMenuButton, reactionMenu);
  assert.match(candidate.text, /tokenized/);
});

test("findOpenReactionLike selects the portal Like option", () => {
  const menu = { tagName: "UL" };
  const trigger = control({ ariaLabel: "Open reactions menu" });
  trigger.closest = () => null;
  const like = control({ ariaLabel: "Like" });
  like.closest = () => menu;
  const root = { querySelectorAll: () => [trigger, like] };

  assert.equal(FeedDom.findOpenReactionLike(root), like);
});

test("Style-2 aria-pressed state prevents unliking an already-liked post", () => {
  const liked = control({ ariaLabel: "Like", pressed: "true" });
  assert.equal(FeedDom.likeState(liked), "liked");
});

test("waitForLiked requires LinkedIn's control state to change", async () => {
  const state = { pressed: "false" };
  const like = control({ ariaLabel: "Like" });
  like.getAttribute = (name) => {
    if (name === "aria-label") return "Like";
    if (name === "aria-pressed") return state.pressed;
    return null;
  };
  const post = {
    querySelectorAll() {
      return [like];
    },
  };
  let polls = 0;

  const confirmed = await FeedDom.waitForLiked(post, like, {
    timeoutMs: 50,
    pollMs: 1,
    sleep: async () => {
      polls += 1;
      if (polls === 2) state.pressed = "true";
    },
  });

  assert.equal(confirmed, true);
  state.pressed = "false";
  assert.equal(
    await FeedDom.waitForLiked(post, like, { timeoutMs: -1, sleep: async () => {} }),
    false,
  );
});

test("comment helpers match LinkedIn's current text-only controls", () => {
  const opener = control({ text: "Comment" });
  const count = control({ text: "16 comments" });
  const submit = control({ text: "Comment" });
  const input = {
    compareDocumentPosition(candidate) {
      return candidate === submit ? 4 : 2;
    },
  };
  const root = {
    querySelector(selector) {
      return selector.includes("textbox") ? input : null;
    },
    querySelectorAll(selector) {
      return selector.includes("button") ? [opener, count, submit] : [];
    },
  };

  assert.equal(FeedDom.findCommentControl(root), opener);
  assert.equal(FeedDom.findCommentInput(root), input);
  assert.equal(FeedDom.findCommentSubmit(root, input, opener), submit);
});

test("comment submit prefers the editor form's current Submit button", () => {
  const opener = control({ text: "Comment" });
  const wrongComment = control({ text: "Comment" });
  const submit = control({ text: "Submit" });
  submit.getAttribute = (name) => (name === "type" ? "submit" : null);
  const form = {
    querySelectorAll(selector) {
      return selector.includes('type="submit"') ? [submit] : [];
    },
  };
  const input = { closest: (selector) => (selector === "form" ? form : null) };
  const root = {
    querySelectorAll() {
      return [opener, wrongComment, submit];
    },
  };

  assert.equal(FeedDom.findCommentSubmit(root, input, opener), submit);
});

test("findMatchingPost reacquires a Style-2 card after a Like remount", () => {
  const oldPost = {
    innerText: "Feed post Ada Lovelace A durable post body about remote teams. Like Comment",
    querySelector: () => ({ href: "https://www.linkedin.com/in/ada/?trk=feed" }),
    querySelectorAll: () => [],
    getAttribute: () => null,
    parentElement: null,
  };
  const identity = FeedDom.postIdentity({ post: oldPost, text: oldPost.innerText, urn: null });

  const like = control({ ariaLabel: "Unreact Like", pressed: "true" });
  const newPost = {
    tagName: "DIV",
    innerText: "Feed post Ada Lovelace A durable post body about remote teams. Unlike Comment",
    textContent: "",
    querySelector(selector) {
      return selector.includes("/in/")
        ? { href: "https://www.linkedin.com/in/ada/", getAttribute: () => "/in/ada/" }
        : null;
    },
    querySelectorAll(selector) {
      return selector === "button" || selector.includes("role") ? [like] : [];
    },
    getAttribute: () => null,
    parentElement: null,
  };
  like.parentElement = newPost;
  like.closest = () => newPost;
  const root = {
    querySelectorAll(selector) {
      if (selector.includes("mainFeed")) return [newPost];
      if (selector.includes('svg[id^="chevron-up"]')) return [];
      return [like];
    },
  };

  assert.equal(FeedDom.findMatchingPost(root, identity), newPost);
});

test("waitForCommentPosted requires both a cleared editor and newly rendered text", async () => {
  const input = { innerText: "a", textContent: "a", isConnected: true };
  let rendered = 0;
  const root = {
    querySelector(selector) {
      return selector.includes("textbox") ? input : null;
    },
    querySelectorAll(selector) {
      if (!selector.includes("span")) return [];
      return Array.from({ length: rendered }, () => ({
        innerText: "a",
        textContent: "a",
        children: [],
      }));
    },
  };
  let polls = 0;
  const confirmed = await FeedDom.waitForCommentPosted(root, input, "a", {
    baselineCount: 0,
    timeoutMs: 50,
    pollMs: 1,
    sleep: async () => {
      polls += 1;
      if (polls === 2) input.innerText = input.textContent = "";
      if (polls === 3) rendered = 1;
    },
  });

  assert.equal(confirmed, true);
  rendered = 0;
  assert.equal(
    await FeedDom.waitForCommentPosted(root, input, "a", {
      baselineCount: 0,
      timeoutMs: -1,
      sleep: async () => {},
    }),
    false,
  );
});

test("waitForCommentPosted rejects a draft preserved in a remounted editor", async () => {
  const oldInput = { innerText: "a", textContent: "a", isConnected: false };
  const remountedInput = { innerText: "a", textContent: "a", isConnected: true };
  const renderedText = { innerText: "a", textContent: "a", children: [] };
  const root = {
    querySelectorAll: () => [renderedText],
  };

  assert.equal(
    await FeedDom.waitForCommentPosted(root, oldInput, "a", {
      baselineCount: 0,
      timeoutMs: -1,
      resolveInput: () => remountedInput,
      sleep: async () => {},
    }),
    false,
  );
});

test("comment workflow uses trusted clicks and counts only confirmed submissions", () => {
  const contentSource = readFileSync(new URL("./content-linkedin.js", import.meta.url), "utf8");
  const start = contentSource.indexOf("async function submitCommentOnPost");
  const end = contentSource.indexOf("// ── Command Handlers", start);
  const submitFlow = contentSource.slice(start, end);

  assert.match(submitFlow, /trustedClick\(commentControl\)/);
  assert.match(submitFlow, /findCommentSubmit\(activePost, liveInput, commentControl\)/);
  assert.match(submitFlow, /countExactRenderedText\(document, commentText\)/);
  assert.match(submitFlow, /waitForCommentPosted\(document, input, commentText/);
  assert.match(submitFlow, /feed\/update\/\$\{postUrn\}/);
  assert.match(submitFlow, /reason: "comment_unconfirmed"/);
  assert.doesNotMatch(submitFlow, /submitButton\.click\(\)/);

  const backgroundSource = readFileSync(new URL("./background.js", import.meta.url), "utf8");
  assert.match(backgroundSource, /msg\.action === "li-cdp-click"/);
  assert.match(backgroundSource, /"Input\.dispatchMouseEvent"/);
});

test("feed actions count only confirmed LinkedIn API or DOM likes", () => {
  const contentSource = readFileSync(new URL("./content-linkedin.js", import.meta.url), "utf8");
  assert.match(contentSource, /async function likeFeedCandidate/);
  assert.match(contentSource, /result\?\.success === true/);
  assert.match(contentSource, /waitForLiked\(post, likeButton/);
  assert.match(contentSource, /reactionMenuButton\.click\(\)/);
  assert.match(contentSource, /menuLike\.click\(\)/);
  assert.doesNotMatch(contentSource, /likeBtn\.click\(\);\s*liked\+\+/);
});

test("Accept-and-DM warm-up reports success only after the page confirms the Like", () => {
  const contentSource = readFileSync(new URL("./content-linkedin.js", import.meta.url), "utf8");
  const start = contentSource.indexOf("async function handleLikeLatestPostOnPage");
  const end = contentSource.indexOf("async function handleScanProfilePosts", start);
  const warmup = contentSource.slice(start, end);

  assert.match(warmup, /waitForLiked\(chosen\.post, likeBtn, \{/);
  assert.match(warmup, /if \(!confirmed\) \{/);
  assert.match(warmup, /errorCode: "like_unconfirmed"/);
  assert.match(warmup, /return \{ success: false,[^}]+errorCode: "like_unconfirmed"/);
  assert.match(warmup, /domConfirmed: true/);
  assert.doesNotMatch(warmup, /aria-pressed didn't flip/);
});

test("the floating panel navigates to the main feed and resumes the run", () => {
  const contentSource = readFileSync(new URL("./content-linkedin.js", import.meta.url), "utf8");
  assert.match(contentSource, /const onFeedPage = \/\^\\\/feed/);
  assert.match(contentSource, /action: "panel-like-and-comment"/);
  assert.match(contentSource, /Opening the main feed, then resuming this run/);

  const backgroundSource = readFileSync(new URL("./background.js", import.meta.url), "utf8");
  const start = backgroundSource.indexOf("async function runPanelLikeAndComment");
  const end = backgroundSource.indexOf("// ── Generic navigate-then-act helper", start);
  const runner = backgroundSource.slice(start, end);
  assert.match(
    runner,
    /chrome\.tabs\.update\(tab\.id, \{ url: "https:\/\/www\.linkedin\.com\/feed\/" \}\)/,
  );
  assert.match(runner, /sendToContentScript\(tab\.id, "li:like-and-comment"/);
  assert.ok(
    runner.indexOf("chrome.tabs.update") < runner.indexOf("sendToContentScript"),
    "navigation must finish before the action is dispatched",
  );
  assert.ok(
    runner.indexOf("result?.success === false") < runner.indexOf("Done! Liked"),
    "a failed result must not be overwritten by a success summary",
  );
});

test("queued comments route through the live extension feed tab", () => {
  const backgroundSource = readFileSync(new URL("./background.js", import.meta.url), "utf8");
  const start = backgroundSource.indexOf('if (action === "li:comment")');
  const end = backgroundSource.indexOf('if (action === "li:ping")', start);
  const route = backgroundSource.slice(start, end);

  assert.ok(start >= 0, "background must accept queued li:comment commands");
  assert.match(route, /navigateLinkedInTab\("https:\/\/www\.linkedin\.com\/feed\/"\)/);
  assert.match(route, /sendToContentScript\(tab\.id, "li:comment"/);
  assert.match(route, /useAI: false/);
});
