package ban

import (
	"testing"

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

func TestParseWeights(t *testing.T) {
	// Exact codes, a bare 3-digit HTTP status (normalized to a family), an exempt code, and "*".
	weights, err := ParseWeights([]string{"42909:10", "403:2", "42908:0", "*:1"})
	require.NoError(t, err)
	require.Equal(t, Weights{"42909": 10, "403*": 2, "42908": 0, "*": 1}, weights)

	// A bare 3-digit HTTP status normalizes to its family.
	weights, err = ParseWeights([]string{"429:5"})
	require.NoError(t, err)
	require.Equal(t, Weights{"429*": 5}, weights)

	// An explicit family key stays as-is.
	weights, err = ParseWeights([]string{"429*:5"})
	require.NoError(t, err)
	require.Equal(t, Weights{"429*": 5}, weights)

	// Weight 0 is valid and means exempt.
	weights, err = ParseWeights([]string{"42908:0"})
	require.NoError(t, err)
	require.Equal(t, Weights{"42908": 0}, weights)

	_, err = ParseWeights([]string{"401"}) // Missing weight
	require.Error(t, err)
	_, err = ParseWeights([]string{"401:-1"}) // Negative weight
	require.Error(t, err)
	_, err = ParseWeights([]string{"401:abc"}) // Non-integer weight
	require.Error(t, err)
	_, err = ParseWeights([]string{"abc:10"}) // Non-numeric key
	require.Error(t, err)
	_, err = ParseWeights([]string{"4*3:10"}) // Star not at the end
	require.Error(t, err)
}

func TestWeights_WeightFor(t *testing.T) {
	weights, err := ParseWeights([]string{"42908:0", "42903:0", "42905:0", "42910:0", "42909:10", "429*:1", "403*:2", "4*:1", "5*:1"})
	require.NoError(t, err)
	// Longest-match-wins: exact 5-digit beats "429*" beats "4*" beats "*".
	require.Equal(t, 0, weights.WeightFor(42908))
	require.Equal(t, 10, weights.WeightFor(42909))
	require.Equal(t, 1, weights.WeightFor(42901))
	require.Equal(t, 2, weights.WeightFor(40311))
	require.Equal(t, 1, weights.WeightFor(40011))
	require.Equal(t, 1, weights.WeightFor(50312))
	// No rule matches (this config has 4*/5* but no "*"), so the implied default weight 1 applies.
	require.Equal(t, 1, weights.WeightFor(30012))
}

func TestWeights_WeightFor_NoStarRuleImpliesWeight1(t *testing.T) {
	// With no "*" rule, a code that matches nothing defaults to weight 1 (can be banned), so the
	// feature can't be silently turned into a no-op by forgetting "*". Explicit codes still win.
	weights, err := ParseWeights([]string{"42908:0", "42909:10"})
	require.NoError(t, err)
	require.Equal(t, 0, weights.WeightFor(42908))  // explicitly exempt
	require.Equal(t, 10, weights.WeightFor(42909)) // explicit
	require.Equal(t, 1, weights.WeightFor(42901))  // unmatched -> implied 1
	require.Equal(t, 1, weights.WeightFor(40001))  // unmatched -> implied 1
}

func TestWeights_WeightFor_ExplicitStarZeroExemptsAll(t *testing.T) {
	// An explicit "*:0" is the opt-out: exempt everything not otherwise weighted.
	weights, err := ParseWeights([]string{"42909:10", "*:0"})
	require.NoError(t, err)
	require.Equal(t, 10, weights.WeightFor(42909)) // explicit
	require.Equal(t, 0, weights.WeightFor(42901))  // *:0 -> exempt everything else
}
