import { afterEach, describe, expect, it, vi } from "vitest";
import { extractMediaFromHtml, fetchNitterRss, stripHtmlTags } from "./rss.js";

// ---------------------------------------------------------------------------
// Sample RSS XML
// ---------------------------------------------------------------------------

const SAMPLE_RSS = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>@testuser</title>
    <link>https://nitter.example.com/testuser</link>
    <item>
      <title>Normal tweet title</title>
      <link>https://nitter.example.com/testuser/status/111</link>
      <description><![CDATA[<p>Hello world! This is a test tweet.</p><img src="https://pbs.twimg.com/media/photo1.jpg"/>]]></description>
      <pubDate>Mon, 10 Mar 2026 12:00:00 GMT</pubDate>
      <guid>https://nitter.example.com/testuser/status/111</guid>
    </item>
    <item>
      <title>RT by @testuser</title>
      <link>https://nitter.example.com/otheruser/status/222</link>
      <description>RT @otheruser: Some retweeted content</description>
      <pubDate>Mon, 10 Mar 2026 11:00:00 GMT</pubDate>
      <guid>https://nitter.example.com/otheruser/status/222</guid>
    </item>
    <item>
      <title>Reply tweet</title>
      <link>https://nitter.example.com/testuser/status/333#m</link>
      <description>@someone I agree with you on this topic</description>
      <pubDate>Mon, 10 Mar 2026 10:00:00 GMT</pubDate>
      <guid>https://nitter.example.com/testuser/status/333#m</guid>
    </item>
    <item>
      <title>Fourth tweet</title>
      <link>https://nitter.example.com/testuser/status/444</link>
      <description>Just another tweet with &amp; entities &lt;3</description>
      <pubDate>Mon, 10 Mar 2026 09:00:00 GMT</pubDate>
      <guid>https://nitter.example.com/testuser/status/444</guid>
    </item>
  </channel>
</rss>`;

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

function mockFetchOk(body: string, status = 200): typeof globalThis.fetch {
  return vi.fn<typeof globalThis.fetch>().mockResolvedValue(
    new Response(body, {
      status,
      statusText: status === 200 ? "OK" : "Error",
    }),
  );
}

afterEach(() => {
  vi.restoreAllMocks();
});

// ---------------------------------------------------------------------------
// Tests: fetchNitterRss — parse sample RSS
// ---------------------------------------------------------------------------

describe("fetchNitterRss", () => {
  it("parses sample RSS XML with multiple items", async () => {
    vi.stubGlobal("fetch", mockFetchOk(SAMPLE_RSS));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "testuser",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;

    expect(result.items.length).toBe(4);

    const first = result.items[0];
    expect(first.title).toBe("Normal tweet title");
    expect(first.content).toBe("Hello world! This is a test tweet.");
    expect(first.link).toBe("https://nitter.example.com/testuser/status/111");
    expect(first.id).toBe("https://nitter.example.com/testuser/status/111");
    expect(first.username).toBe("testuser");
    expect(first.isRetweet).toBe(false);
    expect(first.isReply).toBe(false);
    expect(first.mediaUrls).toEqual(["https://pbs.twimg.com/media/photo1.jpg"]);
    expect(first.pubDateMs).toBeGreaterThan(0);
    expect(first.pubDate).toMatch(/^\d{4}-\d{2}-\d{2}T/);
  });

  it("respects maxItems limit", async () => {
    vi.stubGlobal("fetch", mockFetchOk(SAMPLE_RSS));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "testuser",
      maxItems: 2,
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.items.length).toBe(2);
  });

  it("handles malformed XML gracefully (returns empty items)", async () => {
    vi.stubGlobal("fetch", mockFetchOk("<rss><channel><broken>not valid xml"));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "testuser",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.items).toEqual([]);
  });

  it("handles timeout / fetch error", async () => {
    const delayedFetch = vi.fn<typeof globalThis.fetch>().mockImplementation(
      () =>
        new Promise((_, reject) => {
          setTimeout(() => reject(new Error("The operation was aborted")), 10);
        }),
    );
    vi.stubGlobal("fetch", delayedFetch);

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "testuser",
      timeoutMs: 5,
    });

    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.error).toContain("Fetch failed");
  });

  it("returns error on non-200 status", async () => {
    vi.stubGlobal(
      "fetch",
      vi
        .fn<typeof globalThis.fetch>()
        .mockResolvedValue(new Response("Not Found", { status: 404, statusText: "Not Found" })),
    );

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "testuser",
    });

    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.statusCode).toBe(404);
    expect(result.error).toContain("404");
  });

  it("handles empty feed response (valid XML, no items)", async () => {
    const emptyFeed = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>@emptyuser</title>
    <link>https://nitter.example.com/emptyuser</link>
  </channel>
</rss>`;
    vi.stubGlobal("fetch", mockFetchOk(emptyFeed));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "emptyuser",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.items).toEqual([]);
  });

  it("extracts content from CDATA sections", async () => {
    const cdataFeed = `<rss><channel>
      <item>
        <title><![CDATA[CDATA title]]></title>
        <link>https://nitter.example.com/user/status/555</link>
        <description><![CDATA[<p>Content inside CDATA</p>]]></description>
        <pubDate>Mon, 10 Mar 2026 08:00:00 GMT</pubDate>
        <guid>guid-555</guid>
      </item>
    </channel></rss>`;
    vi.stubGlobal("fetch", mockFetchOk(cdataFeed));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "user",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.items[0].title).toBe("CDATA title");
    expect(result.items[0].content).toBe("Content inside CDATA");
  });

  it("strips @ from username option", async () => {
    vi.stubGlobal("fetch", mockFetchOk(SAMPLE_RSS));

    await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "@testuser",
    });

    expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledWith(
      "https://nitter.example.com/testuser/rss",
      expect.any(Object),
    );
  });
});

// ---------------------------------------------------------------------------
// Tests: retweet detection
// ---------------------------------------------------------------------------

describe("retweet detection", () => {
  it("detects retweet by title prefix 'RT by'", async () => {
    vi.stubGlobal("fetch", mockFetchOk(SAMPLE_RSS));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "testuser",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    const rt = result.items.find((i) => i.id.includes("222"));
    expect(rt).toBeDefined();
    expect(rt!.isRetweet).toBe(true);
  });

  it("detects retweet by content starting with 'RT @'", async () => {
    const rtFeed = `<rss><channel>
      <item>
        <title>Some title</title>
        <link>https://nitter.example.com/user/status/600</link>
        <description>RT @original: forwarded tweet content</description>
        <pubDate>Mon, 10 Mar 2026 07:00:00 GMT</pubDate>
        <guid>guid-600</guid>
      </item>
    </channel></rss>`;
    vi.stubGlobal("fetch", mockFetchOk(rtFeed));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "user",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.items[0].isRetweet).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// Tests: reply detection
// ---------------------------------------------------------------------------

describe("reply detection", () => {
  it("detects reply by content starting with @", async () => {
    vi.stubGlobal("fetch", mockFetchOk(SAMPLE_RSS));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "testuser",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    const reply = result.items.find((i) => i.id.includes("333"));
    expect(reply).toBeDefined();
    expect(reply!.isReply).toBe(true);
  });

  it("detects reply by #m anchor in link", async () => {
    const replyFeed = `<rss><channel>
      <item>
        <title>A reply</title>
        <link>https://nitter.example.com/user/status/700#m</link>
        <description>This is a reply to a thread</description>
        <pubDate>Mon, 10 Mar 2026 06:00:00 GMT</pubDate>
        <guid>guid-700</guid>
      </item>
    </channel></rss>`;
    vi.stubGlobal("fetch", mockFetchOk(replyFeed));

    const result = await fetchNitterRss({
      instanceUrl: "https://nitter.example.com",
      username: "user",
    });

    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.items[0].isReply).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// Tests: stripHtmlTags
// ---------------------------------------------------------------------------

describe("stripHtmlTags", () => {
  it("removes simple HTML tags", () => {
    expect(stripHtmlTags("<p>Hello</p>")).toBe("Hello");
  });

  it("removes nested tags", () => {
    expect(stripHtmlTags("<div><span>Nested</span> text</div>")).toBe("Nested text");
  });

  it("decodes &amp; entity", () => {
    expect(stripHtmlTags("Tom &amp; Jerry")).toBe("Tom & Jerry");
  });

  it("decodes &lt; and &gt; entities", () => {
    expect(stripHtmlTags("a &lt; b &gt; c")).toBe("a < b > c");
  });

  it("decodes &quot; entity", () => {
    expect(stripHtmlTags("&quot;quoted&quot;")).toBe('"quoted"');
  });

  it("decodes &#39; entity", () => {
    expect(stripHtmlTags("it&#39;s fine")).toBe("it's fine");
  });

  it("handles multiple entities in one string", () => {
    expect(stripHtmlTags("a &amp; b &lt; c &gt; d")).toBe("a & b < c > d");
  });

  it("handles string with no tags or entities", () => {
    expect(stripHtmlTags("plain text")).toBe("plain text");
  });

  it("trims whitespace", () => {
    expect(stripHtmlTags("  spaced  ")).toBe("spaced");
  });
});

// ---------------------------------------------------------------------------
// Tests: extractMediaFromHtml
// ---------------------------------------------------------------------------

describe("extractMediaFromHtml", () => {
  it("extracts img src URLs", () => {
    const html = '<p>text</p><img src="https://example.com/photo.jpg"/>';
    expect(extractMediaFromHtml(html)).toEqual(["https://example.com/photo.jpg"]);
  });

  it("extracts video src URLs", () => {
    const html = '<video src="https://example.com/clip.mp4" controls></video>';
    expect(extractMediaFromHtml(html)).toEqual(["https://example.com/clip.mp4"]);
  });

  it("extracts multiple media URLs", () => {
    const html = `
      <img src="https://example.com/a.jpg"/>
      <img src="https://example.com/b.png"/>
      <video src="https://example.com/c.mp4"></video>
    `;
    expect(extractMediaFromHtml(html)).toEqual([
      "https://example.com/a.jpg",
      "https://example.com/b.png",
      "https://example.com/c.mp4",
    ]);
  });

  it("returns empty array when no media found", () => {
    expect(extractMediaFromHtml("<p>No media here</p>")).toEqual([]);
  });

  it("handles single-quoted src attributes", () => {
    const html = "<img src='https://example.com/single.jpg'/>";
    expect(extractMediaFromHtml(html)).toEqual(["https://example.com/single.jpg"]);
  });

  it("handles img with other attributes before src", () => {
    const html = '<img alt="photo" width="100" src="https://example.com/attrs.jpg"/>';
    expect(extractMediaFromHtml(html)).toEqual(["https://example.com/attrs.jpg"]);
  });
});
