package interceptors

import (
	"encoding/json"
	"net/http"
	"net/url"
	"testing"
	"time"

	"github.com/safedep/pmg/config"
	"github.com/safedep/pmg/proxy"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func setCooldownConfig(t *testing.T, cfg config.DependencyCooldownConfig) {
	t.Helper()
	orig := config.Get().Config.DependencyCooldown
	t.Cleanup(func() { config.Get().Config.DependencyCooldown = orig })
	config.Get().Config.DependencyCooldown = cfg
}

func mustParseURL(rawURL string) *url.URL {
	u, err := url.Parse(rawURL)
	if err != nil {
		panic("mustParseURL: " + err.Error())
	}
	return u
}

func TestParseNpmMetadataTime(t *testing.T) {
	handler := newNpmCooldownHandler(nil)

	tests := []struct {
		name          string
		body          []byte
		expectedCount int
		expectError   bool
	}{
		{
			name: "valid metadata with 3 versions",
			body: []byte(`{
				"time": {
					"created": "2020-01-01T00:00:00.000Z",
					"modified": "2024-01-01T00:00:00.000Z",
					"1.0.0": "2020-06-01T00:00:00.000Z",
					"1.0.1": "2021-06-01T00:00:00.000Z",
					"1.0.2": "2022-06-01T00:00:00.000Z"
				}
			}`),
			expectedCount: 3,
		},
		{
			name:          "metadata without time field",
			body:          []byte(`{"name":"foo","version":"1.0.0"}`),
			expectedCount: 0,
		},
		{
			name:          "only skip keys",
			body:          []byte(`{"time":{"created":"2020-01-01T00:00:00.000Z","modified":"2024-01-01T00:00:00.000Z"}}`),
			expectedCount: 0,
		},
		{
			name:        "invalid JSON",
			body:        []byte(`not-json`),
			expectError: true,
		},
		{
			name: "unparseable dates skipped",
			body: []byte(`{
				"time": {
					"1.0.0": "not-a-date",
					"1.0.1": "2022-06-01T00:00:00.000Z"
				}
			}`),
			expectedCount: 1,
		},
		{
			name: "RFC3339 without millis",
			body: []byte(`{
				"time": {
					"1.0.0": "2022-06-01T00:00:00Z"
				}
			}`),
			expectedCount: 1,
		},
		{
			name: "millis precision",
			body: []byte(`{
				"time": {
					"1.0.0": "2022-06-01T00:00:00.000Z"
				}
			}`),
			expectedCount: 1,
		},
		{
			name:        "empty body",
			body:        []byte(``),
			expectError: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			dates, err := handler.parseMetadataTime(tc.body)
			if tc.expectError {
				assert.Error(t, err)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tc.expectedCount, len(dates))
		})
	}
}

func TestParseNpmMetadataTime_CorrectDates(t *testing.T) {
	handler := newNpmCooldownHandler(nil)

	body := []byte(`{
		"time": {
			"created": "2021-01-01T00:00:00.000Z",
			"modified": "2024-01-01T00:00:00.000Z",
			"4.17.20": "2021-02-17T12:00:00.000Z",
			"4.17.21": "2021-05-19T12:00:00.000Z"
		}
	}`)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	assert.Equal(t, 2, len(dates))

	d4_17_20, ok := dates["4.17.20"]
	require.True(t, ok, "expected 4.17.20 in dates")
	assert.Equal(t, 2021, d4_17_20.Year())
	assert.Equal(t, time.February, d4_17_20.Month())
	assert.Equal(t, 17, d4_17_20.Day())

	d4_17_21, ok := dates["4.17.21"]
	require.True(t, ok, "expected 4.17.21 in dates")
	assert.Equal(t, 2021, d4_17_21.Year())
	assert.Equal(t, time.May, d4_17_21.Month())
	assert.Equal(t, 19, d4_17_21.Day())

	_, hasCreated := dates["created"]
	assert.False(t, hasCreated)
	_, hasModified := dates["modified"]
	assert.False(t, hasModified)
}

func buildTestPackument(versions map[string]time.Time, distTags map[string]string) []byte {
	timeMap := map[string]string{
		"created":  "2020-01-01T00:00:00.000Z",
		"modified": "2024-01-01T00:00:00.000Z",
	}
	versionsMap := map[string]any{}
	for v, t := range versions {
		timeMap[v] = t.Format(time.RFC3339)
		versionsMap[v] = map[string]any{"version": v}
	}

	packument := map[string]any{
		"name":      "testpkg",
		"time":      timeMap,
		"versions":  versionsMap,
		"dist-tags": distTags,
	}
	b, _ := json.Marshal(packument)
	return b
}

func TestStripCooldownVersions_MixedVersions(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-30 * 24 * time.Hour), // old
		"1.0.1": now.Add(-10 * 24 * time.Hour), // old
		"1.0.2": now.Add(-1 * 24 * time.Hour),  // too new (within 5d cooldown)
	}
	distTags := map[string]string{"latest": "1.0.2"}
	body := buildTestPackument(versions, distTags)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	newBody, stripped, remaining := handler.stripCooldownVersions(body, dates, 5, nil)
	assert.Equal(t, 1, stripped)
	assert.Equal(t, 2, remaining)

	var result map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(newBody, &result))

	var resultVersions map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(result["versions"], &resultVersions))
	assert.NotContains(t, resultVersions, "1.0.2")
	assert.Contains(t, resultVersions, "1.0.0")
	assert.Contains(t, resultVersions, "1.0.1")

	var resultDistTags map[string]string
	require.NoError(t, json.Unmarshal(result["dist-tags"], &resultDistTags))
	// latest should be repaired to the highest stable eligible version
	assert.Equal(t, "1.0.1", resultDistTags["latest"])

	var resultTime map[string]string
	require.NoError(t, json.Unmarshal(result["time"], &resultTime))
	assert.Contains(t, resultTime, "created")
	assert.Contains(t, resultTime, "modified")
	assert.NotContains(t, resultTime, "1.0.2")
	assert.Contains(t, resultTime, "1.0.0")
	assert.Contains(t, resultTime, "1.0.1")
}

func TestStripCooldownVersions_AllVersionsTooNew(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-1 * 24 * time.Hour), // too new
		"1.0.1": now.Add(-2 * 24 * time.Hour), // too new
	}
	distTags := map[string]string{"latest": "1.0.1"}
	body := buildTestPackument(versions, distTags)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	newBody, stripped, remaining := handler.stripCooldownVersions(body, dates, 5, nil)
	assert.Equal(t, 2, stripped)
	assert.Equal(t, 0, remaining)

	var result map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(newBody, &result))

	var resultDistTags map[string]string
	require.NoError(t, json.Unmarshal(result["dist-tags"], &resultDistTags))
	// No eligible version exists, dist-tag should be removed
	assert.Empty(t, resultDistTags)
}

func TestStripCooldownVersions_NoVersionsTooNew(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-10 * 24 * time.Hour), // old enough
		"1.0.1": now.Add(-20 * 24 * time.Hour), // old enough
	}
	distTags := map[string]string{"latest": "1.0.0"}
	body := buildTestPackument(versions, distTags)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	newBody, stripped, remaining := handler.stripCooldownVersions(body, dates, 5, nil)
	assert.Equal(t, 0, stripped)
	assert.Equal(t, 2, remaining)
	assert.Equal(t, body, newBody) // body unchanged
}

func TestStripCooldownVersions_SingleVersionInCooldown(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-1 * 24 * time.Hour), // too new
	}
	distTags := map[string]string{"latest": "1.0.0"}
	body := buildTestPackument(versions, distTags)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	_, stripped, remaining := handler.stripCooldownVersions(body, dates, 5, nil)
	assert.Equal(t, 1, stripped)
	assert.Equal(t, 0, remaining)
}

func TestStripCooldownVersions_MalformedJSON(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	body := []byte(`not-json`)
	dates := map[string]time.Time{"1.0.0": time.Now().Add(-1 * time.Hour)}

	newBody, stripped, _ := handler.stripCooldownVersions(body, dates, 5, nil)
	assert.Equal(t, 0, stripped)
	assert.Equal(t, body, newBody)
}

// Regression for #275: when the stable version that dist-tags.latest points to is
// stripped, latest must be repaired to the highest *stable* eligible version — never
// a more-recently-published prerelease or platform-specific build (e.g. -win32-arm64).
func TestStripCooldownVersions_LatestRepairedToStableNotPlatform(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	now := time.Now()
	day := 24 * time.Hour
	versions := map[string]time.Time{
		"0.131.0":             now.Add(-40 * day), // old stable
		"0.132.0":             now.Add(-30 * day), // old stable — expected latest after repair
		"0.132.5-win32-arm64": now.Add(-6 * day),  // eligible platform build, newer than 0.132.0
		"0.133.0":             now.Add(-1 * day),  // too new stable (current latest)
		"0.133.0-win32-arm64": now.Add(-1 * day),  // too new platform build
	}
	distTags := map[string]string{"latest": "0.133.0"}
	body := buildTestPackument(versions, distTags)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	newBody, _, _ := handler.stripCooldownVersions(body, dates, 5, nil)

	var result map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(newBody, &result))

	var resultDistTags map[string]string
	require.NoError(t, json.Unmarshal(result["dist-tags"], &resultDistTags))
	assert.Equal(t, "0.132.0", resultDistTags["latest"],
		"latest must be the highest stable eligible version, not a platform/prerelease build")
}

// Non-latest dist-tags whose target is stripped should be removed, not rewritten to
// an unrelated version.
func TestStripCooldownVersions_NonLatestTagRemovedWhenStripped(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	now := time.Now()
	day := 24 * time.Hour
	versions := map[string]time.Time{
		"1.0.0":        now.Add(-30 * day), // eligible stable
		"2.0.0-beta.1": now.Add(-1 * day),  // too new prerelease
	}
	distTags := map[string]string{"latest": "1.0.0", "next": "2.0.0-beta.1"}
	body := buildTestPackument(versions, distTags)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	newBody, _, _ := handler.stripCooldownVersions(body, dates, 5, nil)

	var result map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(newBody, &result))

	var resultDistTags map[string]string
	require.NoError(t, json.Unmarshal(result["dist-tags"], &resultDistTags))
	assert.Equal(t, "1.0.0", resultDistTags["latest"], "eligible latest tag should be untouched")
	assert.NotContains(t, resultDistTags, "next", "stripped non-latest tag should be removed")
}

// A repaired latest must point to a version that still exists in the "versions"
// object. A version present only in "time" (e.g. an unpublished version whose
// timestamp lingers) must not be promoted to latest, or npm would get a dangling tag.
func TestStripCooldownVersions_LatestRepairSkipsVersionsMissingFromPackument(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	old := time.Now().Add(-30 * 24 * time.Hour).Format(time.RFC3339)
	tooNew := time.Now().Add(-1 * 24 * time.Hour).Format(time.RFC3339)

	// "9.9.9" appears in time but NOT in versions; "2.0.0" (latest) is in cooldown.
	body := []byte(`{
		"name": "testpkg",
		"dist-tags": {"latest": "2.0.0"},
		"versions": {
			"1.0.0": {"version": "1.0.0"},
			"1.0.1": {"version": "1.0.1"},
			"2.0.0": {"version": "2.0.0"}
		},
		"time": {
			"1.0.0": "` + old + `",
			"1.0.1": "` + old + `",
			"9.9.9": "` + old + `",
			"2.0.0": "` + tooNew + `"
		}
	}`)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	newBody, _, _ := handler.stripCooldownVersions(body, dates, 5, nil)

	var result map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(newBody, &result))

	var resultDistTags map[string]string
	require.NoError(t, json.Unmarshal(result["dist-tags"], &resultDistTags))
	assert.Equal(t, "1.0.1", resultDistTags["latest"],
		"latest must come from versions present in the packument, not a time-only entry")
}

// Repairing latest must respect the maintainer's dist-tag lineage: when latest is
// pinned to an older line while a higher stable major lives under another channel
// (e.g. next), stripping the fresh latest must fall back within the blessed line,
// not promote the unrelated higher major.
func TestStripCooldownVersions_LatestRepairStaysWithinBlessedLineage(t *testing.T) {
	handler := newNpmCooldownHandler(nil)
	now := time.Now()
	day := 24 * time.Hour
	versions := map[string]time.Time{
		"1.4.0": now.Add(-40 * day), // eligible — previous blessed release
		"1.5.0": now.Add(-1 * day),  // fresh — current latest, stripped
		"2.0.0": now.Add(-30 * day), // eligible higher major, published under `next`
	}
	distTags := map[string]string{"latest": "1.5.0", "next": "2.0.0"}
	body := buildTestPackument(versions, distTags)

	dates, err := handler.parseMetadataTime(body)
	require.NoError(t, err)

	newBody, _, _ := handler.stripCooldownVersions(body, dates, 5, nil)

	var result map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(newBody, &result))

	var resultDistTags map[string]string
	require.NoError(t, json.Unmarshal(result["dist-tags"], &resultDistTags))
	assert.Equal(t, "1.4.0", resultDistTags["latest"],
		"latest must stay within the lineage it was pinned to, not jump to a higher major")
	assert.Equal(t, "2.0.0", resultDistTags["next"], "eligible non-latest tag should be untouched")
}

func makeTestRequestContext(rawURL string) *proxy.RequestContext {
	u := mustParseURL(rawURL)
	return &proxy.RequestContext{
		URL:       u,
		Method:    "GET",
		Headers:   http.Header{},
		Hostname:  u.Host,
		RequestID: "test-req-1",
		StartTime: time.Now(),
	}
}

func TestNpmCooldown_HandleMetadataRequest_OverridesHeaders(t *testing.T) {
	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)

	ctx := makeTestRequestContext("https://registry.npmjs.org/lodash")
	ctx.Headers.Set("Accept", "application/vnd.npm.install-v1+json")
	ctx.Headers.Set("Accept-Encoding", "gzip")
	ctx.Headers.Set("If-None-Match", `"abc123"`)
	ctx.Headers.Set("If-Modified-Since", "Wed, 01 Jan 2025 00:00:00 GMT")

	resp, err := handler.HandleMetadataRequest(ctx, "lodash", 5, "")
	require.NoError(t, err)
	assert.Equal(t, proxy.ActionModifyResponse, resp.Action)
	assert.Equal(t, "application/json", ctx.Headers.Get("Accept"))
	assert.Equal(t, "identity", ctx.Headers.Get("Accept-Encoding"))
	assert.Empty(t, ctx.Headers.Get("If-None-Match"))
	assert.Empty(t, ctx.Headers.Get("If-Modified-Since"))
}

func TestNpmCooldown_HandleMetadataRequest_StripsRecentVersions(t *testing.T) {
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-30 * 24 * time.Hour), // old
		"1.0.1": now.Add(-1 * 24 * time.Hour),  // too new
	}
	distTags := map[string]string{"latest": "1.0.1"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/testpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "testpkg", 5, "")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	newStatus, newHeaders, newBody, err := resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)
	assert.Equal(t, 200, newStatus)
	_ = newHeaders

	var result map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(newBody, &result))

	var resultVersions map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(result["versions"], &resultVersions))
	assert.Contains(t, resultVersions, "1.0.0")
	assert.NotContains(t, resultVersions, "1.0.1")

	var resultDistTags map[string]string
	require.NoError(t, json.Unmarshal(result["dist-tags"], &resultDistTags))
	assert.Equal(t, "1.0.0", resultDistTags["latest"])
}

func TestNpmCooldown_HandleMetadataRequest_NoVersionsInCooldown(t *testing.T) {
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-30 * 24 * time.Hour), // old
		"1.0.1": now.Add(-20 * 24 * time.Hour), // old
	}
	distTags := map[string]string{"latest": "1.0.1"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/testpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "testpkg", 5, "")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	_, _, newBody, err := resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)
	assert.Equal(t, body, newBody)
}

func TestNpmCooldown_HandleMetadataRequest_AllVersionsInCooldown_RecordsStats(t *testing.T) {
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-1 * 24 * time.Hour), // too new
	}
	distTags := map[string]string{"latest": "1.0.0"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/newpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "newpkg", 5, "")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	_, _, _, err = resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)

	blocks := collector.GetCooldownBlocks()
	require.Len(t, blocks, 1)
	assert.Equal(t, "newpkg", blocks[0].Name)
	assert.Equal(t, "1.0.0", blocks[0].Version)
	assert.Equal(t, 5, blocks[0].CooldownDays)

	stats := collector.GetStats()
	assert.Equal(t, 1, stats.CooldownBlockedCount)
	assert.Equal(t, 1, stats.BlockedCount)
}

func TestNpmCooldown_HandleMetadataRequest_AllVersionsInCooldown_ReportsOldestVersion(t *testing.T) {
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-90 * 24 * time.Hour), // oldest — closest to exiting cooldown
		"2.0.0": now.Add(-30 * 24 * time.Hour),
		"2.1.0": now.Add(-1 * 24 * time.Hour), // newest — farthest from exiting cooldown
	}
	distTags := map[string]string{"latest": "2.1.0"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/multipkg")

	resp, err := handler.HandleMetadataRequest(ctx, "multipkg", 100, "")
	require.NoError(t, err)

	_, _, _, err = resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)

	blocks := collector.GetCooldownBlocks()
	require.Len(t, blocks, 1)
	assert.Equal(t, "1.0.0", blocks[0].Version, "should report oldest version (closest to exiting cooldown)")
}

func TestNpmCooldown_HandleMetadataRequest_MalformedJSON_FailOpen(t *testing.T) {
	body := []byte(`not-json`)
	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/badpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "badpkg", 5, "")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	_, _, newBody, err := resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)
	assert.Equal(t, body, newBody)
}

func TestNpmCooldown_HandleMetadataRequest_PinnedVersionInCooldown_RecordsStats(t *testing.T) {
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-30 * 24 * time.Hour), // old — eligible
		"2.0.0": now.Add(-1 * 24 * time.Hour),  // too new (within 5d cooldown)
	}
	distTags := map[string]string{"latest": "2.0.0"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/testpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "testpkg", 5, "2.0.0")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	_, _, _, err = resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)

	blocks := collector.GetCooldownBlocks()
	require.Len(t, blocks, 1)
	assert.Equal(t, "testpkg", blocks[0].Name)
	assert.Equal(t, "2.0.0", blocks[0].Version)
	assert.Equal(t, 5, blocks[0].CooldownDays)

	stats := collector.GetStats()
	assert.Equal(t, 1, stats.CooldownBlockedCount)
	assert.Equal(t, 1, stats.BlockedCount)
}

func TestNpmCooldown_HandleMetadataRequest_PinnedVersionNotInCooldown_NoBlock(t *testing.T) {
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-30 * 24 * time.Hour), // old
		"2.0.0": now.Add(-1 * 24 * time.Hour),  // too new
	}
	distTags := map[string]string{"latest": "2.0.0"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/testpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "testpkg", 5, "1.0.0")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	_, _, _, err = resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)

	blocks := collector.GetCooldownBlocks()
	assert.Empty(t, blocks)
}

func TestNpmCooldown_HandleMetadataRequest_UnpinnedWithRemainingVersions_NoBlock(t *testing.T) {
	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-30 * 24 * time.Hour), // old — eligible
		"2.0.0": now.Add(-1 * 24 * time.Hour),  // too new
	}
	distTags := map[string]string{"latest": "2.0.0"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/testpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "testpkg", 5, "")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	_, _, _, err = resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)

	blocks := collector.GetCooldownBlocks()
	assert.Empty(t, blocks)
}

func TestNpmCooldown_InterceptorDelegation_CooldownEnabled(t *testing.T) {
	setCooldownConfig(t, config.DependencyCooldownConfig{Enabled: true, Days: 5})

	interceptor := NewNpmRegistryInterceptor(nil, NewInMemoryAnalysisCache(), NewAnalysisStatsCollector(), make(chan *ConfirmationRequest, 1), InterceptorContext{})

	ctx := makeTestRequestContext("https://registry.npmjs.org/lodash")
	ctx.Hostname = "registry.npmjs.org"
	ctx.Headers.Set("Accept", "application/vnd.npm.install-v1+json")

	resp, err := interceptor.HandleRequest(ctx)
	require.NoError(t, err)
	assert.Equal(t, proxy.ActionModifyResponse, resp.Action)
	assert.Equal(t, "application/json", ctx.Headers.Get("Accept"))
}

func TestNpmCooldown_InterceptorDelegation_CooldownDisabled(t *testing.T) {
	setCooldownConfig(t, config.DependencyCooldownConfig{Enabled: false, Days: 5})

	interceptor := NewNpmRegistryInterceptor(nil, NewInMemoryAnalysisCache(), NewAnalysisStatsCollector(), make(chan *ConfirmationRequest, 1), InterceptorContext{})

	ctx := makeTestRequestContext("https://registry.npmjs.org/lodash")
	ctx.Hostname = "registry.npmjs.org"
	ctx.Headers.Set("Accept", "application/vnd.npm.install-v1+json")

	resp, err := interceptor.HandleRequest(ctx)
	require.NoError(t, err)
	assert.Equal(t, proxy.ActionAllow, resp.Action)
	// Accept header should NOT be modified when cooldown is disabled
	assert.Equal(t, "application/vnd.npm.install-v1+json", ctx.Headers.Get("Accept"))
}

func TestNpmCooldown_HandleMetadataRequest_PreservesTrustedVersion(t *testing.T) {
	setTrustedPackagesForTest(t, []config.TrustedPackage{{Purl: "pkg:npm/testpkg@2.0.0"}})

	now := time.Now()
	versions := map[string]time.Time{
		"1.0.0": now.Add(-30 * 24 * time.Hour), // old — eligible
		"2.0.0": now.Add(-1 * 24 * time.Hour),  // fresh, trusted — must be preserved
		"2.1.0": now.Add(-1 * 24 * time.Hour),  // fresh, untrusted — must be stripped
	}
	distTags := map[string]string{"latest": "2.1.0"}
	body := buildTestPackument(versions, distTags)

	collector := NewAnalysisStatsCollector()
	handler := newNpmCooldownHandler(collector)
	ctx := makeTestRequestContext("https://registry.npmjs.org/testpkg")

	resp, err := handler.HandleMetadataRequest(ctx, "testpkg", 5, "")
	require.NoError(t, err)
	require.NotNil(t, resp.ResponseModifier)

	_, _, newBody, err := resp.ResponseModifier(200, http.Header{}, body)
	require.NoError(t, err)

	var meta struct {
		Versions map[string]json.RawMessage `json:"versions"`
	}
	require.NoError(t, json.Unmarshal(newBody, &meta))
	assert.Contains(t, meta.Versions, "2.0.0", "trusted fresh version must be preserved")
	assert.NotContains(t, meta.Versions, "2.1.0", "untrusted fresh version must be stripped")
	assert.Contains(t, meta.Versions, "1.0.0", "old version must be preserved")
}

func TestNpmCooldown_TarballRequestBypassesCooldown(t *testing.T) {
	setCooldownConfig(t, config.DependencyCooldownConfig{Enabled: true, Days: 5})

	// Use InsecureInstallation to skip the analyzer (which would fail without a real backend)
	origInsecure := config.Get().InsecureInstallation
	config.Get().InsecureInstallation = true
	t.Cleanup(func() { config.Get().InsecureInstallation = origInsecure })

	interceptor := NewNpmRegistryInterceptor(nil, NewInMemoryAnalysisCache(), NewAnalysisStatsCollector(), make(chan *ConfirmationRequest, 1), InterceptorContext{})

	// Tarball URL has a version component
	ctx := makeTestRequestContext("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz")
	ctx.Hostname = "registry.npmjs.org"

	resp, err := interceptor.HandleRequest(ctx)
	require.NoError(t, err)
	// Tarball should be allowed (bypasses cooldown, goes to analysis)
	assert.Equal(t, proxy.ActionAllow, resp.Action)
	// Accept header should not be set to application/json for tarball requests
	assert.NotEqual(t, proxy.ActionModifyResponse, resp.Action)
}
