/**
 * Per-feed pagination test — the store contract behind the "[Fetch More]"
 * row in a drilled show's episode list (My Shows depth 1) and the Feed
 * page's row.
 *
 * addFeed caches the FULL parsed feed while exposing only the first
 * MAX_EPISODES_SUBSCRIBE (20) episodes. `hasMoreEpisodes` reports when the
 * cache holds more than the loaded window; `loadMoreEpisodes` advances that
 * window in MAX_EPISODES_REFRESH (50) chunks until it is exhausted. This
 * pins:
 *   1. A freshly subscribed feed with a longer cache reports hasMoreEpisodes.
 *   2. loadMoreEpisodes grows that feed's episodes from the cache (no refetch
 *      needed) and hasMoreEpisodes flips false once the window reaches the end.
 *   3. loadMoreEpisodes past the end is a no-op (the feed is untouched).
 */

import { test, expect, beforeAll, afterAll } from "bun:test";
import { mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";

// Point the config dir at a throwaway directory BEFORE importing the stores
// (their module-level init reads it).
const configHome = mkdtempSync(join(tmpdir(), "podtui-pagination-"));
process.env.XDG_CONFIG_HOME = configHome;

import { useFeedStore } from "../src/stores/feed";
import type { Podcast } from "../src/types/podcast";

interface ServedEpisode {
	title: string;
	date: string;
}

let server: ReturnType<typeof Bun.serve> | null = null;
let servedEpisodes: ServedEpisode[] = [];
// Bun runs test files in ONE process, so the store singleton is shared with
// feed-refresh.test.ts / feedless-subscribe.test.ts. Track the feeds we add
// and remove them in afterAll so whichever file runs next sees a pristine
// store (execution order between files is not guaranteed).
const addedFeedIds: string[] = [];

/** XML for the current served episode list (episode ids = feedUrl#index). */
function feedXml(episodes: ServedEpisode[], origin: string): string {
	const items = episodes
		.map(
			(ep, i) => `<item>
	<title>${ep.title}</title>
	<pubDate>${ep.date}</pubDate>
	<enclosure url="${origin}/audio-${i}.mp3" length="12345" type="audio/mpeg"/>
</item>`,
		)
		.join("\n");
	return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
<title>Paged Show</title>
<description>Pagination test feed</description>
${items}
</channel></rss>`;
}

const makePodcast = (feedUrl: string): Podcast => ({
	id: feedUrl,
	title: "Paged Show",
	description: "Pagination test feed",
	author: "tester",
	feedUrl,
	lastUpdated: new Date(),
	isSubscribed: true,
});

beforeAll(() => {
	server = Bun.serve({
		port: 0,
		fetch(req) {
			const url = new URL(req.url);
			if (url.pathname.endsWith(".xml")) {
				return new Response(feedXml(servedEpisodes, url.origin), {
					headers: { "Content-Type": "application/rss+xml" },
				});
			}
			return new Response("not found", { status: 404 });
		},
	});
});

afterAll(() => {
	// Leave the shared singleton as we found it (see addedFeedIds note).
	const store = useFeedStore();
	for (const id of addedFeedIds) store.removeFeed(id);
	server?.stop(true);
	rmSync(configHome, { recursive: true, force: true });
});

test("loadMoreEpisodes advances one feed's window from the cache, then no-ops", async () => {
	const store = useFeedStore();
	// 60 episodes: 20 shown at subscribe, 40 held back in the cache.
	servedEpisodes = Array.from({ length: 60 }, (_, i) => ({
		title: `Ep ${60 - i}`,
		date: new Date(Date.UTC(2026, 0, 1 + i)).toISOString(),
	}));
	const feedUrl = `http://127.0.0.1:${server!.port}/paged.xml`;
	const feed = await store.addFeed(makePodcast(feedUrl), "test-source");
	expect(feed).not.toBeNull();
	const id = feed!.id;
	addedFeedIds.push(id);

	// Subscribe window (MAX_EPISODES_SUBSCRIBE = 20) with more cached.
	expect(store.getFeed(id)!.episodes.length).toBe(20);
	expect(store.hasMoreEpisodes(id)).toBe(true);

	// One load-more covers the remaining 40 (20 + 50 >= 60).
	await store.loadMoreEpisodes(id);
	expect(store.getFeed(id)!.episodes.length).toBe(60);
	expect(store.hasMoreEpisodes(id)).toBe(false);

	// Exhausted: loadMoreEpisodes is a no-op — the feed object is untouched.
	const before = store.getFeed(id)!;
	await store.loadMoreEpisodes(id);
	expect(store.getFeed(id)).toBe(before);
});

test("hasMoreEpisodes stays true across chunked loads until the end", async () => {
	const store = useFeedStore();
	// 120 episodes: 20 shown, 100 cached — two 50-episode chunks remaining.
	servedEpisodes = Array.from({ length: 120 }, (_, i) => ({
		title: `Ep ${120 - i}`,
		date: new Date(Date.UTC(2026, 0, 1 + i)).toISOString(),
	}));
	const feedUrl = `http://127.0.0.1:${server!.port}/paged-chunked.xml`;
	const feed = await store.addFeed(makePodcast(feedUrl), "test-source");
	expect(feed).not.toBeNull();
	const id = feed!.id;
	addedFeedIds.push(id);

	await store.loadMoreEpisodes(id);
	expect(store.getFeed(id)!.episodes.length).toBe(70);
	expect(store.hasMoreEpisodes(id)).toBe(true);

	await store.loadMoreEpisodes(id);
	expect(store.getFeed(id)!.episodes.length).toBe(120);
	expect(store.hasMoreEpisodes(id)).toBe(false);
});
