//go:build unix && !aix && !solaris

package hotnews

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sync"
	"syscall"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestFeedCacheLockContentionFailsFastAndFallsBack(t *testing.T) {
	now := time.Date(2026, 8, 17, 1, 2, 3, 0, time.UTC)
	cache := testFeedCache(t, &now)
	catalog := mustTestPlatform(t, "weibo")
	require.NoError(t, cache.ensureOwnedDirectories())
	fixed := filepath.Join(cache.root, "weibo.json")
	oldFeed := testFeed("weibo", 1)
	require.NoError(t, os.WriteFile(fixed, []byte(oldFeed), 0o600))
	require.NoError(t, os.Chtimes(fixed, now, now))

	lockHolder, err := os.Open(cache.root)
	require.NoError(t, err)
	require.NoError(t, syscall.Flock(int(lockHolder.Fd()), syscall.LOCK_EX))
	var releaseOnce sync.Once
	releaseLock := func() {
		releaseOnce.Do(func() {
			_ = syscall.Flock(int(lockHolder.Fd()), syscall.LOCK_UN)
			_ = lockHolder.Close()
		})
	}
	t.Cleanup(releaseLock)

	type readResult struct {
		payload json.RawMessage
		hit     bool
	}
	readDone := make(chan readResult, 1)
	go func() {
		payload, hit := cache.read(catalog)
		readDone <- readResult{payload: payload, hit: hit}
	}()
	read := awaitContendedCacheOperation(t, readDone, releaseLock)
	require.False(t, read.hit)
	require.Nil(t, read.payload)

	newFeed := json.RawMessage(testFeed("weibo", 2))
	writeDone := make(chan error, 1)
	go func() {
		writeDone <- cache.write(catalog, newFeed)
	}()
	writeErr := awaitContendedCacheOperation(t, writeDone, releaseLock)
	require.Error(t, writeErr)
	require.True(t,
		errors.Is(writeErr, syscall.EWOULDBLOCK) ||
			errors.Is(writeErr, syscall.EAGAIN),
	)

	origin := newStubOrigin(t, statusResponder(200, string(newFeed)))
	handler := handlersWithCache(cache)[ActionGetTrending]
	client := testClient(t, origin.server)
	type handlerResult struct {
		payload json.RawMessage
		err     error
	}
	handlerDone := make(chan handlerResult, 1)
	go func() {
		payload, err := handler(
			context.Background(),
			client,
			nil,
			json.RawMessage(`{"platform_id":"weibo"}`),
		)
		handlerDone <- handlerResult{payload: payload, err: err}
	}()
	handled := awaitContendedCacheOperation(t, handlerDone, releaseLock)
	require.NoError(t, handled.err)
	require.Equal(t, newFeed, handled.payload)
	require.Len(t, origin.recorded(), 1)

	stored, err := os.ReadFile(fixed)
	require.NoError(t, err)
	require.Equal(t, oldFeed, string(stored),
		"lock contention must not overwrite the existing cache pathname")
}

func awaitContendedCacheOperation[T any](
	t *testing.T,
	done <-chan T,
	releaseLock func(),
) T {
	t.Helper()
	select {
	case result := <-done:
		return result
	case <-time.After(time.Second):
		releaseLock()
		<-done
		t.Fatal("cache operation waited for the held root lock")
		var zero T
		return zero
	}
}

func mustTestPlatform(t *testing.T, id string) platform {
	t.Helper()
	entry, ok := platformByID(id)
	require.True(t, ok)
	return entry
}
