package cache

import (
	"bytes"
	"container/heap"
	"context"
	"errors"
	"math"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v3"
	"github.com/stretchr/testify/require"
	"github.com/tinylib/msgp/msgp"
	"github.com/valyala/fasthttp"
)

var errCacheWriteBudget = errors.New("write budget exhausted")

// alwaysErrWriter fails on every write.
type alwaysErrWriter struct{}

func (alwaysErrWriter) Write([]byte) (int, error) { return 0, errCacheWriteBudget }

// encodeMsgErrorSweep drives the per-write error branches of an EncodeMsg
// implementation. The msgp writer enforces an 18-byte minimum buffer, so a
// single failing writer only surfaces an error at fixed flush boundaries.
// Sweeping the buffer size moves the first flush through the encode sequence,
// exercising a different write's error branch on each iteration.
func encodeMsgErrorSweep(t *testing.T, marshaledLen int, encode func(*msgp.Writer) error) {
	t.Helper()

	// An always-failing writer must surface an error for every buffer size,
	// either on the first mid-encode flush or on the final Flush, so assert per
	// iteration rather than once overall.
	for sz := 18; sz <= marshaledLen+18; sz++ {
		w := msgp.NewWriterSize(alwaysErrWriter{}, sz)
		err := encode(w)
		if err == nil {
			err = w.Flush()
		}
		require.Error(t, err, "expected writer failure for buffer size %d", sz)
	}
}

func populatedItem() item {
	return item{
		headers: []cachedHeader{
			{key: []byte("X-A"), value: []byte("1")},
			{key: []byte("X-B"), value: []byte("two")},
		},
		body:            []byte("response body"),
		ctype:           []byte("text/plain"),
		cencoding:       []byte("gzip"),
		cacheControl:    []byte("max-age=60"),
		expires:         []byte("Wed, 21 Oct 2026 07:28:00 GMT"),
		etag:            []byte(`"abc123"`),
		date:            1,
		status:          200,
		age:             2,
		exp:             3,
		ttl:             4,
		forceRevalidate: true,
		revalidate:      true,
		shareable:       true,
		private:         true,
		heapidx:         5,
	}
}

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

	v := populatedItem()
	bts, err := v.MarshalMsg(nil)
	require.NoError(t, err)

	var out item
	left, err := out.UnmarshalMsg(bts)
	require.NoError(t, err)
	require.Empty(t, left)
	require.Equal(t, v, out)
}

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

	v := populatedItem()
	var buf bytes.Buffer
	w := msgp.NewWriter(&buf)
	require.NoError(t, v.EncodeMsg(w))
	require.NoError(t, w.Flush())

	var out item
	require.NoError(t, out.DecodeMsg(msgp.NewReader(&buf)))
	require.Equal(t, v, out)
}

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

	v := populatedItem()
	full, err := v.MarshalMsg(nil)
	require.NoError(t, err)

	for i := range len(full) {
		prefix := full[:i]

		var out item
		_, uerr := out.UnmarshalMsg(prefix)
		require.Error(t, uerr, "UnmarshalMsg should fail on prefix len %d", i)

		var dec item
		require.Error(t, dec.DecodeMsg(msgp.NewReader(bytes.NewReader(prefix))))
	}
}

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

	v := populatedItem()
	full, err := v.MarshalMsg(nil)
	require.NoError(t, err)

	encodeMsgErrorSweep(t, len(full), v.EncodeMsg)
}

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

	var raw []byte
	raw = msgp.AppendMapHeader(raw, 2)
	raw = msgp.AppendString(raw, "unknownField")
	raw = msgp.AppendString(raw, "ignored")
	raw = msgp.AppendString(raw, "status")
	raw = msgp.AppendInt(raw, 204)

	var out item
	_, err := out.UnmarshalMsg(raw)
	require.NoError(t, err)
	require.Equal(t, 204, out.status)

	var dec item
	require.NoError(t, dec.DecodeMsg(msgp.NewReader(bytes.NewReader(raw))))
	require.Equal(t, 204, dec.status)
}

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

	// headers array exceeds the configured limit of 1024.
	var raw []byte
	raw = msgp.AppendMapHeader(raw, 1)
	raw = msgp.AppendString(raw, "headers")
	raw = msgp.AppendArrayHeader(raw, 1025)

	var out item
	_, err := out.UnmarshalMsg(raw)
	require.ErrorIs(t, err, msgp.ErrLimitExceeded)

	var dec item
	require.ErrorIs(t, dec.DecodeMsg(msgp.NewReader(bytes.NewReader(raw))), msgp.ErrLimitExceeded)
}

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

	v := cachedHeader{key: []byte("Content-Type"), value: []byte("text/html")}
	bts, err := v.MarshalMsg(nil)
	require.NoError(t, err)

	var out cachedHeader
	_, err = out.UnmarshalMsg(bts)
	require.NoError(t, err)
	require.Equal(t, v, out)

	for i := range len(bts) {
		var trunc cachedHeader
		_, terr := trunc.UnmarshalMsg(bts[:i])
		require.Error(t, terr)

		var dec cachedHeader
		require.Error(t, dec.DecodeMsg(msgp.NewReader(bytes.NewReader(bts[:i]))))
	}
}

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

	v := cachedHeader{key: []byte("Content-Type"), value: []byte("text/html")}

	var buf bytes.Buffer
	w := msgp.NewWriter(&buf)
	require.NoError(t, v.EncodeMsg(w))
	require.NoError(t, w.Flush())

	var out cachedHeader
	require.NoError(t, out.DecodeMsg(msgp.NewReader(&buf)))
	require.Equal(t, v, out)

	full, err := v.MarshalMsg(nil)
	require.NoError(t, err)
	encodeMsgErrorSweep(t, len(full), v.EncodeMsg)
}

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

	// Unknown field is skipped.
	var raw []byte
	raw = msgp.AppendMapHeader(raw, 2)
	raw = msgp.AppendString(raw, "zz")
	raw = msgp.AppendString(raw, "ignored")
	raw = msgp.AppendString(raw, "key")
	raw = msgp.AppendBytes(raw, []byte("X-Test"))

	var out cachedHeader
	_, err := out.UnmarshalMsg(raw)
	require.NoError(t, err)
	require.Equal(t, []byte("X-Test"), out.key)

	var dec cachedHeader
	require.NoError(t, dec.DecodeMsg(msgp.NewReader(bytes.NewReader(raw))))
	require.Equal(t, []byte("X-Test"), dec.key)

	// key over the 512-byte limit.
	var bigKey []byte
	bigKey = msgp.AppendMapHeader(bigKey, 1)
	bigKey = msgp.AppendString(bigKey, "key")
	bigKey = msgp.AppendBytes(bigKey, make([]byte, 513))
	var k cachedHeader
	_, err = k.UnmarshalMsg(bigKey)
	require.ErrorIs(t, err, msgp.ErrLimitExceeded)
	var kDec cachedHeader
	require.ErrorIs(t, kDec.DecodeMsg(msgp.NewReader(bytes.NewReader(bigKey))), msgp.ErrLimitExceeded)

	// value over the 16384-byte limit.
	var bigVal []byte
	bigVal = msgp.AppendMapHeader(bigVal, 1)
	bigVal = msgp.AppendString(bigVal, "value")
	bigVal = msgp.AppendBytes(bigVal, make([]byte, 16385))
	var vv cachedHeader
	_, err = vv.UnmarshalMsg(bigVal)
	require.ErrorIs(t, err, msgp.ErrLimitExceeded)
	var vvDec cachedHeader
	require.ErrorIs(t, vvDec.DecodeMsg(msgp.NewReader(bytes.NewReader(bigVal))), msgp.ErrLimitExceeded)
}

// Test_item_Decode_FieldLimits covers the per-field ErrLimitExceeded branches
// of the item byte fields.
func Test_item_Decode_FieldLimits(t *testing.T) {
	t.Parallel()

	cases := []struct {
		field string
		size  int
	}{
		{"ctype", 257},
		{"cencoding", 129},
		{"cacheControl", 2049},
		{"expires", 129},
		{"etag", 257},
	}
	for _, tc := range cases {
		var raw []byte
		raw = msgp.AppendMapHeader(raw, 1)
		raw = msgp.AppendString(raw, tc.field)
		raw = msgp.AppendBytes(raw, make([]byte, tc.size))

		var out item
		_, err := out.UnmarshalMsg(raw)
		require.ErrorIs(t, err, msgp.ErrLimitExceeded, "field %s", tc.field)

		var dec item
		require.ErrorIs(t, dec.DecodeMsg(msgp.NewReader(bytes.NewReader(raw))), msgp.ErrLimitExceeded, "field %s", tc.field)
	}
}

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

	h := &indexedHeap{}

	// put inserts entries and returns tracking indices.
	idxA := h.put("a", 30, 1)
	idxB := h.put("b", 10, 2)
	idxC := h.put("c", 20, 3)
	require.Equal(t, 3, h.Len())

	// The lowest expiration must come out first.
	key, _ := h.removeFirst()
	require.Equal(t, "b", key)

	// Remove an arbitrary tracked entry.
	key, _ = h.remove(idxC)
	require.Equal(t, "c", key)
	_ = idxA
	_ = idxB

	// Directly exercise the heap.Interface Push method (not used by put()).
	h2 := &indexedHeap{indices: []int{0}}
	heap.Push(h2, heapEntry{key: "z", exp: 5, idx: 0})
	require.Equal(t, 1, h2.Len())
	require.Equal(t, "z", h2.entries[0].key)
}

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

	v, ok := parseUintDirective([]byte("60"))
	require.True(t, ok)
	require.Equal(t, uint64(60), v)

	_, ok = parseUintDirective(nil)
	require.False(t, ok)

	_, ok = parseUintDirective([]byte("not-a-number"))
	require.False(t, ok)

	// Invalid max-age values are ignored when parsing full directives.
	parsed := parseResponseCacheControl([]byte("max-age=abc, s-maxage=xyz"))
	require.False(t, parsed.maxAgeSet)
	require.False(t, parsed.sMaxAgeSet)
}

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

	require.False(t, allowsSharedCacheDirectives(responseCacheControl{hasPrivate: true}))
	require.True(t, allowsSharedCacheDirectives(responseCacheControl{hasPublic: true}))
	require.True(t, allowsSharedCacheDirectives(responseCacheControl{sMaxAgeSet: true}))
	require.True(t, allowsSharedCacheDirectives(responseCacheControl{mustRevalidate: true}))
	require.True(t, allowsSharedCacheDirectives(responseCacheControl{proxyRevalidate: true}))
	require.False(t, allowsSharedCacheDirectives(responseCacheControl{}))
}

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

	require.True(t, allowsSharedCacheStorage(responseCacheControl{hasPublic: true}))
	require.True(t, allowsSharedCacheStorage(responseCacheControl{sMaxAgeSet: true}))
	require.False(t, allowsSharedCacheStorage(responseCacheControl{}))

	// The revalidate directives part company with allowsSharedCacheDirectives:
	// they say when a stale entry may be reused, not that the body is
	// impersonal, and this middleware never revalidates.
	require.False(t, allowsSharedCacheStorage(responseCacheControl{mustRevalidate: true}))
	require.False(t, allowsSharedCacheStorage(responseCacheControl{proxyRevalidate: true}))

	// private wins over an accompanying opt-in. The store path rejects private
	// well before the cookie gate, so this is the predicate holding its own
	// contract rather than a reachable branch.
	require.False(t, allowsSharedCacheStorage(responseCacheControl{hasPrivate: true, hasPublic: true}))
	require.False(t, allowsSharedCacheStorage(responseCacheControl{hasPrivate: true, sMaxAgeSet: true}))
}

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

	// secondsToTime clamps values beyond math.MaxInt64.
	require.Equal(t, time.Unix(math.MaxInt64, 0).UTC(), secondsToTime(math.MaxUint64))
	require.Equal(t, time.Unix(5, 0).UTC(), secondsToTime(5))

	// secondsToDuration clamps on overflow.
	require.Equal(t, time.Duration(math.MaxInt64), secondsToDuration(math.MaxUint64))
	require.Equal(t, 5*time.Second, secondsToDuration(5))
}

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

	fn := func(lines [][]byte) string {
		return string(appendAuthHash(nil, lines))
	}
	got := fn([][]byte{[]byte("Bearer token")})
	require.Len(t, got, hexLen)
	// Stable for the same input, and reuses the pooled framing buffer on the
	// second call.
	require.Equal(t, got, fn([][]byte{[]byte("Bearer token")}))

	// Appended to whatever the caller is already building, not returned alone.
	require.Equal(t, "key|auth="+got, string(appendAuthHash([]byte("key|auth="), [][]byte{[]byte("Bearer token")})))

	// Length-prefixed, so a split that concatenates to the same bytes does not
	// land on the same digest.
	require.NotEqual(t, fn([][]byte{[]byte("ab")}), fn([][]byte{[]byte("a"), []byte("b")}))
}

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

	// Storage GetWithContext returns an error.
	storage := newFailingCacheStorage()
	storage.errs["get|k"] = errors.New("boom")
	m := newManager(storage, true)
	_, err := m.get(context.Background(), "k")
	require.ErrorContains(t, err, redactedKey)

	// Stored bytes fail to unmarshal.
	storage2 := newFailingCacheStorage()
	storage2.data["k"] = []byte{0xff, 0xff, 0xff}
	m2 := newManager(storage2, false)
	_, err = m2.get(context.Background(), "k")
	require.ErrorContains(t, err, "unmarshal")

	// Cache miss.
	m3 := newManager(newFailingCacheStorage(), false)
	_, err = m3.get(context.Background(), "missing")
	require.ErrorIs(t, err, errCacheMiss)

	// Memory-backed unexpected type.
	m4 := newManager(nil, false)
	m4.memory.Set("k", "not-an-item", time.Minute)
	_, err = m4.get(context.Background(), "k")
	require.ErrorContains(t, err, "unexpected entry type")
}

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

	// Storage error.
	storage := newFailingCacheStorage()
	storage.errs["get|k"] = errors.New("boom")
	m := newManager(storage, false)
	_, err := m.getRaw(context.Background(), "k")
	require.ErrorContains(t, err, "boom")

	// Storage hit.
	storage2 := newFailingCacheStorage()
	storage2.data["k"] = []byte("raw-value")
	m2 := newManager(storage2, false)
	raw, err := m2.getRaw(context.Background(), "k")
	require.NoError(t, err)
	require.Equal(t, []byte("raw-value"), raw)

	// Memory hit.
	m3 := newManager(nil, false)
	require.NoError(t, m3.setRaw(context.Background(), "k", []byte("mem"), time.Minute))
	raw, err = m3.getRaw(context.Background(), "k")
	require.NoError(t, err)
	require.Equal(t, []byte("mem"), raw)

	// Memory unexpected raw type.
	m4 := newManager(nil, false)
	m4.memory.Set("k", 12345, time.Minute)
	_, err = m4.getRaw(context.Background(), "k")
	require.ErrorContains(t, err, "unexpected raw entry type")

	// Miss.
	m5 := newManager(newFailingCacheStorage(), false)
	_, err = m5.getRaw(context.Background(), "missing")
	require.ErrorIs(t, err, errCacheMiss)
}

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

	storage := newFailingCacheStorage()
	storage.errs["set|k"] = errors.New("boom")
	m := newManager(storage, false)

	it := m.acquire()
	it.status = 200
	err := m.set(context.Background(), "k", it, time.Minute)
	require.ErrorContains(t, err, "boom")

	// setRaw storage error.
	require.ErrorContains(t, m.setRaw(context.Background(), "k", []byte("v"), time.Minute), "boom")
}

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

	storage := newFailingCacheStorage()
	m := newManager(storage, false)

	it := m.acquire()
	it.status = 201
	it.body = []byte("hello")
	require.NoError(t, m.set(context.Background(), "k", it, time.Minute))

	got, err := m.get(context.Background(), "k")
	require.NoError(t, err)
	require.Equal(t, 201, got.status)
	require.Equal(t, []byte("hello"), got.body)
}

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

	m := newManager(newFailingCacheStorage(), false)
	ctx := context.Background()

	// Empty names is a no-op store.
	require.NoError(t, storeVaryManifest(ctx, m, "mk", nil, time.Minute))

	// Missing manifest -> no names, no error.
	names, ok, err := loadVaryManifest(ctx, m, "mk")
	require.NoError(t, err)
	require.False(t, ok)
	require.Nil(t, names)

	// Store and load a real manifest.
	require.NoError(t, storeVaryManifest(ctx, m, "mk", []string{"accept", "accept-encoding"}, time.Minute))
	names, ok, err = loadVaryManifest(ctx, m, "mk")
	require.NoError(t, err)
	require.True(t, ok)
	require.Equal(t, []string{"accept", "accept-encoding"}, names)

	// A wildcard manifest is treated as uncacheable.
	require.NoError(t, m.setRaw(ctx, "star", []byte("*"), time.Minute))
	names, ok, err = loadVaryManifest(ctx, m, "star")
	require.NoError(t, err)
	require.False(t, ok)
	require.Nil(t, names)

	// Storage error propagates.
	storage := newFailingCacheStorage()
	storage.errs["get|mk"] = errors.New("boom")
	mErr := newManager(storage, false)
	_, _, err = loadVaryManifest(ctx, mErr, "mk")
	require.ErrorContains(t, err, "boom")
}

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

	fn := func(names []string, hdr *fasthttp.RequestHeader, normalized bool) string {
		return string(appendVaryKey(nil, names, hdr, normalized))
	}

	var hdr fasthttp.RequestHeader
	hdr.Set("Accept", "application/json")
	hdr.Set("Accept-Encoding", "gzip")

	key := fn([]string{"accept", "accept-encoding"}, &hdr, true)
	require.Contains(t, key, "|vary|")
	// Deterministic for the same inputs.
	require.Equal(t, key, fn([]string{"accept", "accept-encoding"}, &hdr, true))
	// varyKey is the same suffix appended to a base key, in one allocation.
	require.Equal(t, "base"+key, varyKey("base", []string{"accept", "accept-encoding"}, &hdr, true))
}

// Test_appendVaryKey_RepeatedFieldLines pins that every field line of a
// Vary'd header reaches the key.
//
// A name may arrive on more than one line, and the split is equivalent to the
// comma-joined form on the wire (RFC 9110 Section 5.2). The key used to hash
// only the first line, so a request carrying two X-Tenant lines and one
// carrying just the first shared an entry — the exact cross-request mixing
// Vary exists to prevent.
func Test_appendVaryKey_RepeatedFieldLines(t *testing.T) {
	t.Parallel()

	fn := func(names []string, hdr *fasthttp.RequestHeader, normalized bool) string {
		return string(appendVaryKey(nil, names, hdr, normalized))
	}

	key := func(values ...string) string {
		var hdr fasthttp.RequestHeader
		for _, v := range values {
			hdr.Add("X-Tenant", v)
		}
		return fn([]string{"x-tenant"}, &hdr, true)
	}

	both := key("public", "acme-private")
	require.NotEqual(t, both, key("public"), "a dropped field line must change the key")
	require.NotEqual(t, both, key("acme-private"))
	require.NotEqual(t, both, key(), "an absent header must not key like a present one")
	// Order is part of the value, so a reordering is a different variant.
	require.NotEqual(t, both, key("acme-private", "public"))
	// The framing is injective: two lines must not hash like one joined line.
	require.NotEqual(t, both, key("publicacme-private"))
	require.NotEqual(t, both, key("public,acme-private"))
	// And the per-value length prefix is what makes it so. The assertions above
	// differ in how many lines there are, which is hashed on its own, so they
	// hold even without the prefix; these two split the same bytes the same
	// number of ways and collide without it.
	require.NotEqual(t, key("ab", "c"), key("a", "bc"))
	require.NotEqual(t, key("a", "bb", "c"), key("aa", "b", "c"))
	// Same input, same key.
	require.Equal(t, both, key("public", "acme-private"))
}

// Test_Cache_VaryRepeatedFieldLines is the end-to-end form: a request that
// sends one field line must not be served the entry stored for a request that
// sent two.
func Test_Cache_VaryRepeatedFieldLines(t *testing.T) {
	t.Parallel()

	app := fiber.New()
	app.Use(New(Config{Expiration: time.Minute}))
	app.Get("/", func(c fiber.Ctx) error {
		c.Set("Vary", "X-Tenant")
		values := c.Request().Header.PeekAll("X-Tenant")
		parts := make([]string, 0, len(values))
		for _, v := range values {
			parts = append(parts, string(v)+";")
		}
		return c.SendString("tenant:" + strings.Join(parts, ""))
	})

	do := func(lines ...string) (body, cacheStatus string) { //nolint:nonamedreturns // names document the pair
		req := fasthttp.AcquireRequest()
		defer fasthttp.ReleaseRequest(req)
		req.Header.SetMethod(fiber.MethodGet)
		req.SetRequestURI("/")
		req.SetHost("example.com")
		for _, l := range lines {
			req.Header.Add("X-Tenant", l)
		}
		fctx := &fasthttp.RequestCtx{}
		fctx.Init(req, nil, nil)
		app.Handler()(fctx)
		return string(fctx.Response.Body()), string(fctx.Response.Header.Peek("X-Cache"))
	}

	body, status := do("public", "acme-private")
	require.Equal(t, cacheMiss, status)
	require.Equal(t, "tenant:public;acme-private;", body)

	body, status = do("public")
	require.Equal(t, cacheMiss, status, "the single-line request must not hit the two-line entry")
	require.Equal(t, "tenant:public;", body)

	// Each variant still caches on its own key.
	body, status = do("public", "acme-private")
	require.Equal(t, cacheHit, status)
	require.Equal(t, "tenant:public;acme-private;", body)

	body, status = do("public")
	require.Equal(t, cacheHit, status)
	require.Equal(t, "tenant:public;", body)
}
