import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { FeedStateManager } from "./feed-state.js";

let tmpDir: string;

beforeEach(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "nitter-feed-state-"));
});

afterEach(async () => {
  await fs.rm(tmpDir, { recursive: true, force: true });
});

describe("FeedStateManager", () => {
  it("load from missing file creates default state", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    expect(mgr.listSubscriptions()).toEqual([]);
  });

  it("load from corrupt file creates default state", async () => {
    const dir = path.join(tmpDir, "plugins", "nitter");
    await fs.mkdir(dir, { recursive: true });
    await fs.writeFile(path.join(dir, "feed-state.json"), "NOT JSON!!!", "utf-8");

    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    expect(mgr.listSubscriptions()).toEqual([]);
  });

  it("save and reload preserves state", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    mgr.addSubscription({ username: "elonmusk", enabled: true, source: "tool" });
    await mgr.save();

    const mgr2 = new FeedStateManager(tmpDir);
    await mgr2.load();

    const subs = mgr2.listSubscriptions();
    expect(subs).toHaveLength(1);
    expect(subs[0].username).toBe("elonmusk");
    expect(subs[0].source).toBe("tool");
    expect(subs[0].enabled).toBe(true);
    expect(subs[0].addedAtMs).toBeGreaterThan(0);
  });

  it("add subscription", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    const sub = mgr.addSubscription({
      username: "testuser",
      label: "Test",
      enabled: true,
      source: "tool",
    });

    expect(sub.username).toBe("testuser");
    expect(sub.label).toBe("Test");
    expect(sub.addedAtMs).toBeGreaterThan(0);
    expect(mgr.listSubscriptions()).toHaveLength(1);
  });

  it("remove subscription (case insensitive)", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    mgr.addSubscription({ username: "TestUser", enabled: true, source: "tool" });
    expect(mgr.listSubscriptions()).toHaveLength(1);

    const removed = mgr.removeSubscription("testuser");
    expect(removed).toBe(true);
    expect(mgr.listSubscriptions()).toHaveLength(0);
  });

  it("remove nonexistent subscription returns false", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    expect(mgr.removeSubscription("nobody")).toBe(false);
  });

  it("duplicate subscription add is no-op", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    const sub1 = mgr.addSubscription({ username: "DupeUser", enabled: true, source: "tool" });
    const sub2 = mgr.addSubscription({ username: "dupeuser", enabled: false, source: "config" });

    expect(mgr.listSubscriptions()).toHaveLength(1);
    // Returns the existing subscription, not a new one
    expect(sub2.addedAtMs).toBe(sub1.addedAtMs);
    expect(sub2.source).toBe("tool"); // original preserved
  });

  it("getSubscription is case insensitive", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    mgr.addSubscription({ username: "MixedCase", enabled: true, source: "tool" });

    expect(mgr.getSubscription("mixedcase")).toBeDefined();
    expect(mgr.getSubscription("MIXEDCASE")).toBeDefined();
    expect(mgr.getSubscription("MixedCase")).toBeDefined();
    expect(mgr.getSubscription("nonexistent")).toBeUndefined();
  });

  it("getPollState returns default for unknown username", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    const ps = mgr.getPollState("unknown");
    expect(ps).toEqual({
      lastPollAtMs: 0,
      seenIds: [],
      consecutiveErrors: 0,
    });
  });

  it("recordPoll with new items returns only unseen", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    mgr.addSubscription({ username: "feeduser", enabled: true, source: "tool" });

    const newItems1 = mgr.recordPoll("feeduser", {
      items: [{ id: "a" }, { id: "b" }, { id: "c" }],
    });
    expect(newItems1).toEqual([{ id: "a" }, { id: "b" }, { id: "c" }]);

    // Second poll with overlap — only "d" is new
    const newItems2 = mgr.recordPoll("feeduser", {
      items: [{ id: "b" }, { id: "c" }, { id: "d" }],
    });
    expect(newItems2).toEqual([{ id: "d" }]);

    const ps = mgr.getPollState("feeduser");
    expect(ps.seenIds).toEqual(["a", "b", "c", "d"]);
    expect(ps.lastItemId).toBe("d");
    expect(ps.consecutiveErrors).toBe(0);
  });

  it("recordPoll caps seenIds at 200", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    mgr.addSubscription({ username: "bigfeed", enabled: true, source: "tool" });

    // Add 210 items in one poll
    const items = Array.from({ length: 210 }, (_, i) => ({ id: `item-${i}` }));
    mgr.recordPoll("bigfeed", { items });

    const ps = mgr.getPollState("bigfeed");
    expect(ps.seenIds).toHaveLength(200);
    // Should keep the newest (last 200), dropping the oldest 10
    expect(ps.seenIds[0]).toBe("item-10");
    expect(ps.seenIds[199]).toBe("item-209");
  });

  it("recordPoll with error increments consecutiveErrors", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    mgr.addSubscription({ username: "errfeed", enabled: true, source: "tool" });

    const result1 = mgr.recordPoll("errfeed", { error: "timeout" });
    expect(result1).toEqual([]);

    const ps1 = mgr.getPollState("errfeed");
    expect(ps1.consecutiveErrors).toBe(1);
    expect(ps1.lastError).toBe("timeout");

    const result2 = mgr.recordPoll("errfeed", { error: "network error" });
    expect(result2).toEqual([]);

    const ps2 = mgr.getPollState("errfeed");
    expect(ps2.consecutiveErrors).toBe(2);
    expect(ps2.lastError).toBe("network error");

    // Successful poll resets errors
    mgr.recordPoll("errfeed", { items: [{ id: "ok-1" }] });
    const ps3 = mgr.getPollState("errfeed");
    expect(ps3.consecutiveErrors).toBe(0);
    expect(ps3.lastError).toBeUndefined();
  });

  it("syncFromConfig adds config feeds and removes stale ones", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    // Initial sync with two config feeds
    mgr.syncFromConfig([
      { username: "config_a", enabled: true },
      { username: "config_b", enabled: true, label: "B Feed" },
    ]);

    expect(mgr.listSubscriptions()).toHaveLength(2);
    expect(mgr.getSubscription("config_a")?.source).toBe("config");
    expect(mgr.getSubscription("config_b")?.label).toBe("B Feed");

    // Second sync removes config_b, adds config_c
    mgr.syncFromConfig([
      { username: "config_a", enabled: true },
      { username: "config_c", enabled: false },
    ]);

    expect(mgr.listSubscriptions()).toHaveLength(2);
    expect(mgr.getSubscription("config_a")).toBeDefined();
    expect(mgr.getSubscription("config_b")).toBeUndefined();
    expect(mgr.getSubscription("config_c")?.enabled).toBe(false);
    expect(mgr.getSubscription("config_c")?.source).toBe("config");
  });

  it("syncFromConfig preserves tool-added subscriptions", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    // Add a tool subscription first
    mgr.addSubscription({ username: "tool_user", enabled: true, source: "tool", label: "My Sub" });

    // Sync config (does not include tool_user)
    mgr.syncFromConfig([{ username: "config_user", enabled: true }]);

    const subs = mgr.listSubscriptions();
    expect(subs).toHaveLength(2);
    expect(mgr.getSubscription("tool_user")?.source).toBe("tool");
    expect(mgr.getSubscription("tool_user")?.label).toBe("My Sub");
    expect(mgr.getSubscription("config_user")?.source).toBe("config");
  });

  it("syncFromConfig upserts existing config feeds", async () => {
    const mgr = new FeedStateManager(tmpDir);
    await mgr.load();

    mgr.syncFromConfig([{ username: "updatable", enabled: true, label: "Old Label" }]);

    const originalAddedAt = mgr.getSubscription("updatable")?.addedAtMs;

    mgr.syncFromConfig([{ username: "updatable", enabled: false, label: "New Label" }]);

    const updated = mgr.getSubscription("updatable");
    expect(updated?.label).toBe("New Label");
    expect(updated?.enabled).toBe(false);
    // Preserves original addedAtMs
    expect(updated?.addedAtMs).toBe(originalAddedAt);
  });
});
