package hotnews

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

func TestProductionFeedCacheUsesHotNewsNamespace(t *testing.T) {
	t.Parallel()

	cacheBase := filepath.Join(os.TempDir(), "agentteam-extension-cache")
	providerDirectory := filepath.Join(cacheBase, "hotnews")
	root := filepath.Join(providerDirectory, "v1")

	cache := newProductionFeedCache()

	require.Equal(t, root, cache.root)
	require.Equal(t, []string{cacheBase, providerDirectory, root}, cache.ownedDirectories)
	require.Equal(t, ".hotnews-cache-", feedCacheTempPrefix)
}

func TestFeedCacheSequentialHitUsesOneRequest(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	feed := testFeed("weibo", 1)
	origin := newStubOrigin(t, statusResponder(http.StatusOK, feed))
	client := testClient(t, origin.server)

	first, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		client, `{"platform_id":"weibo"}`, cache)
	require.NoError(t, err)
	second, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		client, `{"platform_id":"weibo"}`, cache)
	require.NoError(t, err)

	require.Equal(t, feed, string(first))
	require.Equal(t, first, second)
	require.Len(t, origin.recorded(), 1)
}

func TestFeedCacheExpiryRefetchesWithoutSleeping(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	var calls atomic.Int32
	origin := newStubOrigin(t, func(writer http.ResponseWriter, _ *http.Request) {
		call := calls.Add(1)
		_, _ = writer.Write([]byte(testFeed("weibo", int(call))))
	})
	client := testClient(t, origin.server)

	first, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		client, `{"platform_id":"weibo"}`, cache)
	require.NoError(t, err)
	now = now.Add(feedCacheTTL)
	second, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		client, `{"platform_id":"weibo"}`, cache)
	require.NoError(t, err)

	require.Equal(t, testFeed("weibo", 1), string(first))
	require.Equal(t, testFeed("weibo", 2), string(second))
	require.Equal(t, int32(2), calls.Load())
}

func TestFeedCacheRejectsAndReplacesBadEntries(t *testing.T) {
	setups := map[string]func(*testing.T, string, time.Time){
		"malformed": func(t *testing.T, path string, now time.Time) {
			require.NoError(t, os.WriteFile(path, []byte(
				`{"status":"success","id":"weibo","updatedTime":1,"items":null}`),
				0o600))
			require.NoError(t, os.Chtimes(path, now, now))
		},
		"wrong platform": func(t *testing.T, path string, now time.Time) {
			require.NoError(t, os.WriteFile(path, []byte(testFeed("baidu", 1)), 0o600))
			require.NoError(t, os.Chtimes(path, now, now))
		},
		"expired": func(t *testing.T, path string, now time.Time) {
			require.NoError(t, os.WriteFile(path, []byte(testFeed("weibo", 1)), 0o600))
			expired := now.Add(-feedCacheTTL)
			require.NoError(t, os.Chtimes(path, expired, expired))
		},
		"oversized": func(t *testing.T, path string, now time.Time) {
			require.NoError(t, os.WriteFile(path,
				[]byte(strings.Repeat("x", runner.MaxResponseBytes+1)), 0o600))
			require.NoError(t, os.Chtimes(path, now, now))
		},
		"symlink": func(t *testing.T, path string, _ time.Time) {
			target := filepath.Join(t.TempDir(), "outside.json")
			require.NoError(t, os.WriteFile(target, []byte(testFeed("weibo", 1)), 0o600))
			require.NoError(t, os.Symlink(target, path))
		},
		"directory": func(t *testing.T, path string, _ time.Time) {
			require.NoError(t, os.Mkdir(path, 0o700))
		},
	}
	for name, setup := range setups {
		name, setup := name, setup
		t.Run(name, func(t *testing.T) {
			now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
			cache := testFeedCache(t, &now)
			require.NoError(t, os.MkdirAll(cache.root, 0o700))
			fixed := filepath.Join(cache.root, "weibo.json")
			setup(t, fixed, now)

			upstream := testFeed("weibo", 2)
			origin := newStubOrigin(t, statusResponder(http.StatusOK, upstream))
			output, err := invokeWithCache(t, context.Background(), ActionGetTrending,
				testClient(t, origin.server), `{"platform_id":"weibo"}`, cache)

			require.NoError(t, err)
			require.Equal(t, upstream, string(output))
			require.Len(t, origin.recorded(), 1)
			info, err := os.Lstat(fixed)
			require.NoError(t, err)
			require.True(t, info.Mode().IsRegular())
			stored, err := os.ReadFile(fixed)
			require.NoError(t, err)
			require.Equal(t, upstream, string(stored))
		})
	}
}

func TestFeedCacheUsesPrivateFixedFilesWithoutHistory(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	var calls atomic.Int32
	origin := newStubOrigin(t, func(writer http.ResponseWriter, _ *http.Request) {
		_, _ = writer.Write([]byte(testFeed("weibo", int(calls.Add(1)))))
	})
	client := testClient(t, origin.server)

	_, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		client, `{"platform_id":"weibo"}`, cache)
	require.NoError(t, err)
	now = now.Add(feedCacheTTL)
	_, err = invokeWithCache(t, context.Background(), ActionGetTrending,
		client, `{"platform_id":"weibo"}`, cache)
	require.NoError(t, err)

	rootInfo, err := os.Stat(cache.root)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o700), rootInfo.Mode().Perm())
	entries, err := os.ReadDir(cache.root)
	require.NoError(t, err)
	require.Len(t, entries, 1)
	require.Equal(t, "weibo.json", entries[0].Name())
	fileInfo, err := entries[0].Info()
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), fileInfo.Mode().Perm())
	require.True(t, now.Equal(fileInfo.ModTime()))
}

func TestFeedCacheHitNormalizesOwnedPermissions(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	base := t.TempDir()
	require.NoError(t, os.Chmod(base, 0o755))
	cacheBase := filepath.Join(base, "agentteam-extension-cache")
	providerDirectory := filepath.Join(cacheBase, "hotnews")
	root := filepath.Join(providerDirectory, "v1")
	ownedDirectories := []string{cacheBase, providerDirectory, root}
	require.NoError(t, os.MkdirAll(root, 0o755))
	for _, directory := range ownedDirectories {
		require.NoError(t, os.Chmod(directory, 0o755))
	}
	fixed := filepath.Join(root, "weibo.json")
	feed := testFeed("weibo", 1)
	require.NoError(t, os.WriteFile(fixed, []byte(feed), 0o644))
	require.NoError(t, os.Chmod(fixed, 0o644))
	require.NoError(t, os.Chtimes(fixed, now, now))
	cache := newFeedCache(root, ownedDirectories, feedCacheTTL,
		func() time.Time { return now })
	origin := newStubOrigin(t,
		statusResponder(http.StatusOK, testFeed("weibo", 2)))

	output, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		testClient(t, origin.server), `{"platform_id":"weibo"}`, cache)

	require.NoError(t, err)
	require.Equal(t, feed, string(output))
	require.Empty(t, origin.recorded())
	for _, directory := range ownedDirectories {
		info, err := os.Lstat(directory)
		require.NoError(t, err)
		require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
	}
	baseInfo, err := os.Lstat(base)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o755), baseInfo.Mode().Perm(),
		"cache must not chmod ancestors it does not explicitly own")
	fileInfo, err := os.Lstat(fixed)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), fileInfo.Mode().Perm())
}

func TestFeedCacheFirstUseSweepIsClosedAndAgeBounded(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	require.NoError(t, os.MkdirAll(cache.root, 0o700))
	stale := now.Add(-feedCacheTTL)
	fresh := now.Add(-feedCacheTTL / 2)

	writeCacheFixture(t, cache.root, "baidu.json", testFeed("baidu", 1), stale)
	writeCacheFixture(t, cache.root, "douyin.json", testFeed("douyin", 1), fresh)
	writeCacheFixture(t, cache.root, feedCacheTempPrefix+"stale", "partial", stale)
	writeCacheFixture(t, cache.root, feedCacheTempPrefix+"fresh", "partial", fresh)
	writeCacheFixture(t, cache.root, "unknown.json", "unknown", stale)
	writeCacheFixture(t, cache.root, "notes.txt", "unknown", stale)

	origin := newStubOrigin(t,
		statusResponder(http.StatusOK, testFeed("weibo", 1)))
	_, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		testClient(t, origin.server), `{"platform_id":"weibo"}`, cache)
	require.NoError(t, err)

	require.NoFileExists(t, filepath.Join(cache.root, "baidu.json"))
	require.NoFileExists(t, filepath.Join(cache.root, feedCacheTempPrefix+"stale"))
	for _, preserved := range []string{
		"douyin.json",
		feedCacheTempPrefix + "fresh",
		"unknown.json",
		"notes.txt",
	} {
		require.FileExists(t, filepath.Join(cache.root, preserved))
	}
	require.FileExists(t, filepath.Join(cache.root, "weibo.json"))
}

func TestFeedCacheFailuresFallBackToUpstream(t *testing.T) {
	t.Run("root is file", func(t *testing.T) {
		now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
		root := filepath.Join(t.TempDir(), "cache")
		require.NoError(t, os.WriteFile(root, []byte("occupied"), 0o600))
		cache := newFeedCache(
			root, []string{root}, feedCacheTTL, func() time.Time { return now })
		assertCacheFailureFallsBack(t, cache)
	})

	t.Run("missing non-owned parent", func(t *testing.T) {
		now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
		root := filepath.Join(t.TempDir(), "missing", "non-owned", "cache")
		cache := newFeedCache(root, []string{root}, feedCacheTTL,
			func() time.Time { return now })
		assertCacheFailureFallsBack(t, cache)
	})

	t.Run("owned directory symlink", func(t *testing.T) {
		now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
		base := t.TempDir()
		cacheBase := filepath.Join(base, "agentteam-extension-cache")
		require.NoError(t, os.Mkdir(cacheBase, 0o755))
		outside := t.TempDir()
		require.NoError(t, os.Chmod(outside, 0o755))
		providerDirectory := filepath.Join(cacheBase, "hotnews")
		require.NoError(t, os.Symlink(outside, providerDirectory))
		root := filepath.Join(providerDirectory, "v1")
		cache := newFeedCache(
			root,
			[]string{cacheBase, providerDirectory, root},
			feedCacheTTL,
			func() time.Time { return now },
		)

		assertCacheFailureFallsBack(t, cache)

		linkInfo, err := os.Lstat(providerDirectory)
		require.NoError(t, err)
		require.NotZero(t, linkInfo.Mode()&os.ModeSymlink)
		outsideInfo, err := os.Lstat(outside)
		require.NoError(t, err)
		require.Equal(t, os.FileMode(0o755), outsideInfo.Mode().Perm(),
			"cache must not chmod an owned-directory symlink target")
		require.NoDirExists(t, filepath.Join(outside, "v1"))
	})
}

func TestFeedCacheDoesNotCacheFailedOrMalformedResponses(t *testing.T) {
	cases := map[string]struct {
		status int
		body   string
	}{
		"5xx": {
			status: http.StatusBadGateway,
			body:   `{"error":"unavailable"}`,
		},
		"malformed 2xx": {
			status: http.StatusOK,
			body:   `{"status":"success","id":"weibo","updatedTime":1,"items":null}`,
		},
	}
	for name, testCase := range cases {
		name, testCase := name, testCase
		t.Run(name, func(t *testing.T) {
			now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
			cache := testFeedCache(t, &now)
			origin := newStubOrigin(t, statusResponder(testCase.status, testCase.body))
			client := testClient(t, origin.server)

			for range 2 {
				_, err := invokeWithCache(t, context.Background(), ActionGetTrending,
					client, `{"platform_id":"weibo"}`, cache)
				require.Error(t, err)
			}

			require.Len(t, origin.recorded(), 2)
			require.NoFileExists(t, filepath.Join(cache.root, "weibo.json"))
		})
	}
}

func TestFeedCacheCleansTemporaryFileWhenRenameFails(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	require.NoError(t, os.MkdirAll(cache.root, 0o700))
	fixed := filepath.Join(cache.root, "weibo.json")
	require.NoError(t, os.Mkdir(fixed, 0o700))
	require.NoError(t, os.WriteFile(
		filepath.Join(fixed, "occupied"), []byte("occupied"), 0o600))
	require.NoError(t, os.Chtimes(fixed, now, now))

	feed := testFeed("weibo", 1)
	origin := newStubOrigin(t, statusResponder(http.StatusOK, feed))
	output, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		testClient(t, origin.server), `{"platform_id":"weibo"}`, cache)

	require.NoError(t, err)
	require.Equal(t, feed, string(output))
	entries, err := os.ReadDir(cache.root)
	require.NoError(t, err)
	require.Len(t, entries, 1)
	require.Equal(t, "weibo.json", entries[0].Name())
	require.True(t, entries[0].IsDir())
}

func TestFeedCacheDoesNotFollowFixedFileSymlinkForPermissions(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	require.NoError(t, os.MkdirAll(cache.root, 0o700))
	target := filepath.Join(t.TempDir(), "outside.json")
	outsideFeed := testFeed("weibo", 1)
	require.NoError(t, os.WriteFile(target, []byte(outsideFeed), 0o644))
	require.NoError(t, os.Chmod(target, 0o644))
	fixed := filepath.Join(cache.root, "weibo.json")
	require.NoError(t, os.Symlink(target, fixed))
	upstreamFeed := testFeed("weibo", 2)
	origin := newStubOrigin(t, statusResponder(http.StatusOK, upstreamFeed))

	output, err := invokeWithCache(t, context.Background(), ActionGetTrending,
		testClient(t, origin.server), `{"platform_id":"weibo"}`, cache)

	require.NoError(t, err)
	require.Equal(t, upstreamFeed, string(output))
	require.Len(t, origin.recorded(), 1)
	targetInfo, err := os.Lstat(target)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o644), targetInfo.Mode().Perm(),
		"cache must never chmod a cache-file symlink target")
	targetBody, err := os.ReadFile(target)
	require.NoError(t, err)
	require.Equal(t, outsideFeed, string(targetBody))
	fixedInfo, err := os.Lstat(fixed)
	require.NoError(t, err)
	require.True(t, fixedInfo.Mode().IsRegular())
	require.Equal(t, os.FileMode(0o600), fixedInfo.Mode().Perm())
}

func TestFeedCacheAllowsDifferentPlatformsToFetchIndependently(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	started := make(chan string, 2)
	release := make(chan struct{})
	var releaseOnce sync.Once
	t.Cleanup(func() { releaseOnce.Do(func() { close(release) }) })
	origin := newStubOrigin(t, func(writer http.ResponseWriter, request *http.Request) {
		id := request.URL.Query().Get("id")
		started <- id
		<-release
		_, _ = writer.Write([]byte(testFeed(id, 1)))
	})
	client := testClient(t, origin.server)
	handler := handlersWithCache(cache)[ActionGetTrending]

	ids := []string{"weibo", "baidu"}
	outputs := make([]json.RawMessage, len(ids))
	errors := make([]error, len(ids))
	var done sync.WaitGroup
	done.Add(len(ids))
	for index, id := range ids {
		index, id := index, id
		go func() {
			defer done.Done()
			outputs[index], errors[index] = handler(
				context.Background(),
				client,
				nil,
				json.RawMessage(fmt.Sprintf(`{"platform_id":%q}`, id)),
			)
		}()
	}

	observed := make(map[string]bool, len(ids))
	for range ids {
		select {
		case id := <-started:
			observed[id] = true
		case <-time.After(5 * time.Second):
			releaseOnce.Do(func() { close(release) })
			done.Wait()
			require.FailNow(t, "platform fetches did not run independently")
		}
	}
	releaseOnce.Do(func() { close(release) })
	done.Wait()

	require.Equal(t, map[string]bool{"weibo": true, "baidu": true}, observed)
	for index, id := range ids {
		require.NoError(t, errors[index])
		require.Equal(t, testFeed(id, 1), string(outputs[index]))
	}
}

func TestFeedCacheCanceledLeaderDoesNotCancelSharedFetch(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	feed := testFeed("weibo", 1)
	upstreamStarted := make(chan struct{})
	releaseUpstream := make(chan struct{})
	var releaseOnce sync.Once
	t.Cleanup(func() { releaseOnce.Do(func() { close(releaseUpstream) }) })
	var startOnce sync.Once
	var calls atomic.Int32
	origin := newStubOrigin(t, func(writer http.ResponseWriter, _ *http.Request) {
		calls.Add(1)
		startOnce.Do(func() { close(upstreamStarted) })
		<-releaseUpstream
		_, _ = writer.Write([]byte(feed))
	})
	client := testClient(t, origin.server)
	handler := handlersWithCache(cache)[ActionGetTrending]
	leaderContext, cancelLeader := context.WithCancel(context.Background())
	defer cancelLeader()

	type result struct {
		output json.RawMessage
		err    error
	}
	leaderResult := make(chan result, 1)
	go func() {
		output, err := handler(
			leaderContext,
			client,
			nil,
			json.RawMessage(`{"platform_id":"weibo"}`),
		)
		leaderResult <- result{output: output, err: err}
	}()
	<-upstreamStarted

	followerStarted := make(chan struct{})
	followerResult := make(chan result, 1)
	go func() {
		close(followerStarted)
		output, err := handler(
			context.Background(),
			client,
			nil,
			json.RawMessage(`{"platform_id":"weibo"}`),
		)
		followerResult <- result{output: output, err: err}
	}()
	<-followerStarted
	cancelLeader()

	var leader result
	select {
	case leader = <-leaderResult:
	case <-time.After(5 * time.Second):
		releaseOnce.Do(func() { close(releaseUpstream) })
		require.FailNow(t, "canceled leader did not return promptly")
	}
	require.Equal(t, context.Canceled, leader.err)
	require.Nil(t, leader.output)

	releaseOnce.Do(func() { close(releaseUpstream) })
	var follower result
	select {
	case follower = <-followerResult:
	case <-time.After(5 * time.Second):
		require.FailNow(t, "healthy follower did not receive the shared result")
	}
	require.NoError(t, follower.err)
	require.Equal(t, feed, string(follower.output))
	require.Equal(t, int32(1), calls.Load())
}

func TestFeedCacheCanceledFollowerReturnsBeforeSharedFetchCompletes(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	feed := testFeed("weibo", 1)
	upstreamStarted := make(chan struct{})
	releaseUpstream := make(chan struct{})
	var releaseOnce sync.Once
	t.Cleanup(func() { releaseOnce.Do(func() { close(releaseUpstream) }) })
	var startOnce sync.Once
	var calls atomic.Int32
	origin := newStubOrigin(t, func(writer http.ResponseWriter, _ *http.Request) {
		calls.Add(1)
		startOnce.Do(func() { close(upstreamStarted) })
		<-releaseUpstream
		_, _ = writer.Write([]byte(feed))
	})
	client := testClient(t, origin.server)
	handler := handlersWithCache(cache)[ActionGetTrending]

	type result struct {
		output json.RawMessage
		err    error
	}
	leaderResult := make(chan result, 1)
	go func() {
		output, err := handler(
			context.Background(),
			client,
			nil,
			json.RawMessage(`{"platform_id":"weibo"}`),
		)
		leaderResult <- result{output: output, err: err}
	}()
	<-upstreamStarted

	followerContext, cancelFollower := context.WithCancel(context.Background())
	followerStarted := make(chan struct{})
	followerResult := make(chan result, 1)
	go func() {
		close(followerStarted)
		output, err := handler(
			followerContext,
			client,
			nil,
			json.RawMessage(`{"platform_id":"weibo"}`),
		)
		followerResult <- result{output: output, err: err}
	}()
	<-followerStarted
	cancelFollower()

	var follower result
	select {
	case follower = <-followerResult:
	case <-time.After(5 * time.Second):
		releaseOnce.Do(func() { close(releaseUpstream) })
		<-leaderResult
		require.FailNow(t, "canceled follower waited for the shared fetch")
	}
	require.Equal(t, context.Canceled, follower.err)
	require.Nil(t, follower.output)

	releaseOnce.Do(func() { close(releaseUpstream) })
	var leader result
	select {
	case leader = <-leaderResult:
	case <-time.After(5 * time.Second):
		require.FailNow(t, "healthy leader did not complete")
	}
	require.NoError(t, leader.err)
	require.Equal(t, feed, string(leader.output))
	require.Equal(t, int32(1), calls.Load())
}

func TestFeedCacheRootLockSerializesDeleteBeforeReplace(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	readerCache := testFeedCache(t, &now)
	writerCache := newFeedCache(
		readerCache.root,
		[]string{readerCache.root},
		feedCacheTTL,
		func() time.Time { return now },
	)
	require.NoError(t, readerCache.ensureOwnedDirectories())
	fixed := filepath.Join(readerCache.root, "weibo.json")
	require.NoError(t, os.WriteFile(fixed, []byte("old"), 0o600))
	replacement := filepath.Join(readerCache.root, feedCacheTempPrefix+"replacement")
	newFeed := testFeed("weibo", 2)
	require.NoError(t, os.WriteFile(replacement, []byte(newFeed), 0o600))

	readerLocked := make(chan struct{})
	allowReaderDelete := make(chan struct{})
	readerDone := make(chan error, 1)
	go func() {
		readerDone <- readerCache.withRootLock(func() error {
			close(readerLocked)
			<-allowReaderDelete
			return os.Remove(fixed)
		})
	}()
	<-readerLocked

	writerEntered := make(chan struct{})
	writerAttempted := make(chan struct{})
	writerDone := make(chan error, 1)
	go func() {
		close(writerAttempted)
		writerDone <- writerCache.withRootLock(func() error {
			close(writerEntered)
			return os.Rename(replacement, fixed)
		})
	}()
	<-writerAttempted

	var contentionErr error
	select {
	case contentionErr = <-writerDone:
	case <-time.After(time.Second):
		close(allowReaderDelete)
		<-readerDone
		<-writerDone
		require.FailNow(t, "replacement waited for the cache-root lock")
	}
	require.Error(t, contentionErr)
	select {
	case <-writerEntered:
		close(allowReaderDelete)
		<-readerDone
		require.FailNow(t, "replacement entered while delete held the cache-root lock")
	default:
	}
	require.FileExists(t, replacement)

	close(allowReaderDelete)
	require.NoError(t, <-readerDone)
	require.NoError(t, writerCache.withRootLock(func() error {
		return os.Rename(replacement, fixed)
	}))
	require.FileExists(t, fixed)
	stored, err := os.ReadFile(fixed)
	require.NoError(t, err)
	require.Equal(t, newFeed, string(stored),
		"the old reader must not delete the replacement pathname")
}

func TestFeedCacheConcurrentColdMissUsesOneRequest(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	feed := testFeed("weibo", 1)
	started := make(chan struct{})
	release := make(chan struct{})
	var startOnce sync.Once
	var calls atomic.Int32
	origin := newStubOrigin(t, func(writer http.ResponseWriter, _ *http.Request) {
		calls.Add(1)
		startOnce.Do(func() { close(started) })
		<-release
		_, _ = writer.Write([]byte(feed))
	})
	client := testClient(t, origin.server)
	handler := handlersWithCache(cache)[ActionGetTrending]

	const workers = 24
	outputs := make([]json.RawMessage, workers)
	errors := make([]error, workers)
	start := make(chan struct{})
	var ready sync.WaitGroup
	var done sync.WaitGroup
	ready.Add(workers)
	done.Add(workers)
	for index := range workers {
		index := index
		go func() {
			defer done.Done()
			ready.Done()
			<-start
			outputs[index], errors[index] = handler(
				context.Background(),
				client,
				nil,
				json.RawMessage(`{"platform_id":"weibo"}`),
			)
		}()
	}
	ready.Wait()
	close(start)
	<-started
	close(release)
	done.Wait()

	require.Equal(t, int32(1), calls.Load())
	for index := range workers {
		require.NoError(t, errors[index])
		require.Equal(t, feed, string(outputs[index]))
	}
	outputs[0][0] = '!'
	require.Equal(t, feed, string(outputs[1]),
		"singleflight callers must receive independent byte slices")
}

func testFeedCache(t *testing.T, now *time.Time) *feedCache {
	t.Helper()
	root := filepath.Join(t.TempDir(), "cache")
	return newFeedCache(
		root,
		[]string{root},
		feedCacheTTL,
		func() time.Time { return *now },
	)
}

func testFeed(id string, updatedTime int) string {
	entry, ok := platformByID(id)
	if !ok {
		panic("test feed requested an unknown platform: " + id)
	}
	return fmt.Sprintf(
		`{"status":"success","id":%q,"updatedTime":%d,`+
			`"items":[{"url":"https://news.%s/topic"}]}`,
		id, updatedTime, entry.expectedDomain,
	)
}

func writeCacheFixture(
	t *testing.T,
	root string,
	name string,
	body string,
	modTime time.Time,
) {
	t.Helper()
	path := filepath.Join(root, name)
	require.NoError(t, os.WriteFile(path, []byte(body), 0o600))
	require.NoError(t, os.Chtimes(path, modTime, modTime))
}

func assertCacheFailureFallsBack(t *testing.T, cache *feedCache) {
	t.Helper()
	feed := testFeed("weibo", 1)
	origin := newStubOrigin(t, statusResponder(http.StatusOK, feed))
	client := testClient(t, origin.server)
	for range 2 {
		output, err := invokeWithCache(t, context.Background(), ActionGetTrending,
			client, `{"platform_id":"weibo"}`, cache)
		require.NoError(t, err)
		require.Equal(t, feed, string(output))
	}
	require.Len(t, origin.recorded(), 2)
}
