package pkg_test

import (
	"math/big"
	"testing"
	"time"

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

	"github.com/rwadurian/mpc-system/pkg/utils"
)

func TestGenerateID(t *testing.T) {
	t.Run("should generate unique IDs", func(t *testing.T) {
		id1 := utils.GenerateID()
		id2 := utils.GenerateID()

		assert.NotEqual(t, id1, id2)
	})
}

func TestParseUUID(t *testing.T) {
	t.Run("should parse valid UUID", func(t *testing.T) {
		id := utils.GenerateID()
		parsed, err := utils.ParseUUID(id.String())
		require.NoError(t, err)
		assert.Equal(t, id, parsed)
	})

	t.Run("should fail on invalid UUID", func(t *testing.T) {
		_, err := utils.ParseUUID("invalid-uuid")
		assert.Error(t, err)
	})
}

func TestIsValidUUID(t *testing.T) {
	t.Run("should return true for valid UUID", func(t *testing.T) {
		id := utils.GenerateID()
		assert.True(t, utils.IsValidUUID(id.String()))
	})

	t.Run("should return false for invalid UUID", func(t *testing.T) {
		assert.False(t, utils.IsValidUUID("not-a-uuid"))
	})
}

func TestJSON(t *testing.T) {
	t.Run("should marshal and unmarshal JSON", func(t *testing.T) {
		original := map[string]interface{}{
			"key":   "value",
			"count": float64(42),
		}

		data, err := utils.ToJSON(original)
		require.NoError(t, err)

		var result map[string]interface{}
		err = utils.FromJSON(data, &result)
		require.NoError(t, err)

		assert.Equal(t, original["key"], result["key"])
		assert.Equal(t, original["count"], result["count"])
	})
}

func TestNowUTC(t *testing.T) {
	t.Run("should return UTC time", func(t *testing.T) {
		now := utils.NowUTC()
		assert.Equal(t, time.UTC, now.Location())
	})
}

func TestTimePtr(t *testing.T) {
	t.Run("should return pointer to time", func(t *testing.T) {
		now := time.Now()
		ptr := utils.TimePtr(now)

		require.NotNil(t, ptr)
		assert.Equal(t, now, *ptr)
	})
}

func TestBigIntBytes(t *testing.T) {
	t.Run("should convert big.Int to bytes and back", func(t *testing.T) {
		original, _ := new(big.Int).SetString("12345678901234567890", 10)
		bytes := utils.BigIntToBytes(original)
		assert.Len(t, bytes, 32)

		result := utils.BytesToBigInt(bytes)
		assert.Equal(t, 0, original.Cmp(result))
	})

	t.Run("should handle nil big.Int", func(t *testing.T) {
		bytes := utils.BigIntToBytes(nil)
		assert.Len(t, bytes, 32)
		assert.Equal(t, make([]byte, 32), bytes)
	})
}

func TestStringSliceContains(t *testing.T) {
	t.Run("should find existing value", func(t *testing.T) {
		slice := []string{"a", "b", "c"}
		assert.True(t, utils.StringSliceContains(slice, "b"))
	})

	t.Run("should not find missing value", func(t *testing.T) {
		slice := []string{"a", "b", "c"}
		assert.False(t, utils.StringSliceContains(slice, "d"))
	})

	t.Run("should handle empty slice", func(t *testing.T) {
		assert.False(t, utils.StringSliceContains([]string{}, "a"))
	})
}

func TestStringSliceRemove(t *testing.T) {
	t.Run("should remove existing value", func(t *testing.T) {
		slice := []string{"a", "b", "c"}
		result := utils.StringSliceRemove(slice, "b")

		assert.Len(t, result, 2)
		assert.Contains(t, result, "a")
		assert.Contains(t, result, "c")
		assert.NotContains(t, result, "b")
	})

	t.Run("should not modify slice if value not found", func(t *testing.T) {
		slice := []string{"a", "b", "c"}
		result := utils.StringSliceRemove(slice, "d")

		assert.Len(t, result, 3)
	})
}

func TestUniqueStrings(t *testing.T) {
	t.Run("should return unique strings", func(t *testing.T) {
		slice := []string{"a", "b", "a", "c", "b"}
		result := utils.UniqueStrings(slice)

		assert.Len(t, result, 3)
		assert.Contains(t, result, "a")
		assert.Contains(t, result, "b")
		assert.Contains(t, result, "c")
	})

	t.Run("should preserve order", func(t *testing.T) {
		slice := []string{"c", "a", "b", "a"}
		result := utils.UniqueStrings(slice)

		assert.Equal(t, []string{"c", "a", "b"}, result)
	})
}

func TestTruncateString(t *testing.T) {
	t.Run("should truncate long string", func(t *testing.T) {
		s := "hello world"
		result := utils.TruncateString(s, 5)
		assert.Equal(t, "hello", result)
	})

	t.Run("should not truncate short string", func(t *testing.T) {
		s := "hi"
		result := utils.TruncateString(s, 5)
		assert.Equal(t, "hi", result)
	})
}

func TestSafeString(t *testing.T) {
	t.Run("should return string value", func(t *testing.T) {
		s := "test"
		result := utils.SafeString(&s)
		assert.Equal(t, "test", result)
	})

	t.Run("should return empty string for nil", func(t *testing.T) {
		result := utils.SafeString(nil)
		assert.Equal(t, "", result)
	})
}

func TestPointerHelpers(t *testing.T) {
	t.Run("StringPtr", func(t *testing.T) {
		ptr := utils.StringPtr("test")
		require.NotNil(t, ptr)
		assert.Equal(t, "test", *ptr)
	})

	t.Run("IntPtr", func(t *testing.T) {
		ptr := utils.IntPtr(42)
		require.NotNil(t, ptr)
		assert.Equal(t, 42, *ptr)
	})

	t.Run("BoolPtr", func(t *testing.T) {
		ptr := utils.BoolPtr(true)
		require.NotNil(t, ptr)
		assert.True(t, *ptr)
	})
}

func TestCoalesce(t *testing.T) {
	t.Run("should return first non-zero value", func(t *testing.T) {
		result := utils.Coalesce("", "", "value", "other")
		assert.Equal(t, "value", result)
	})

	t.Run("should return zero if all values are zero", func(t *testing.T) {
		result := utils.Coalesce("", "", "")
		assert.Equal(t, "", result)
	})

	t.Run("should work with ints", func(t *testing.T) {
		result := utils.Coalesce(0, 0, 42, 100)
		assert.Equal(t, 42, result)
	})
}

func TestMapKeys(t *testing.T) {
	t.Run("should return all keys", func(t *testing.T) {
		m := map[string]int{"a": 1, "b": 2, "c": 3}
		keys := utils.MapKeys(m)

		assert.Len(t, keys, 3)
		assert.Contains(t, keys, "a")
		assert.Contains(t, keys, "b")
		assert.Contains(t, keys, "c")
	})

	t.Run("should return empty slice for empty map", func(t *testing.T) {
		m := map[string]int{}
		keys := utils.MapKeys(m)
		assert.Empty(t, keys)
	})
}

func TestMapValues(t *testing.T) {
	t.Run("should return all values", func(t *testing.T) {
		m := map[string]int{"a": 1, "b": 2, "c": 3}
		values := utils.MapValues(m)

		assert.Len(t, values, 3)
		assert.Contains(t, values, 1)
		assert.Contains(t, values, 2)
		assert.Contains(t, values, 3)
	})
}

func TestMinMax(t *testing.T) {
	t.Run("Min should return smaller value", func(t *testing.T) {
		assert.Equal(t, 1, utils.Min(1, 2))
		assert.Equal(t, 1, utils.Min(2, 1))
		assert.Equal(t, -5, utils.Min(-5, 0))
	})

	t.Run("Max should return larger value", func(t *testing.T) {
		assert.Equal(t, 2, utils.Max(1, 2))
		assert.Equal(t, 2, utils.Max(2, 1))
		assert.Equal(t, 0, utils.Max(-5, 0))
	})
}

func TestClamp(t *testing.T) {
	t.Run("should clamp value to range", func(t *testing.T) {
		assert.Equal(t, 5, utils.Clamp(5, 0, 10))    // within range
		assert.Equal(t, 0, utils.Clamp(-5, 0, 10))   // below min
		assert.Equal(t, 10, utils.Clamp(15, 0, 10))  // above max
	})
}

func TestMaskString(t *testing.T) {
	t.Run("should mask middle of string", func(t *testing.T) {
		result := utils.MaskString("1234567890", 2)
		assert.Equal(t, "12******90", result)
	})

	t.Run("should mask short strings completely", func(t *testing.T) {
		result := utils.MaskString("1234", 3)
		assert.Equal(t, "****", result)
	})
}

func TestRetry(t *testing.T) {
	t.Run("should succeed on first attempt", func(t *testing.T) {
		attempts := 0
		err := utils.Retry(3, time.Millisecond, func() error {
			attempts++
			return nil
		})

		assert.NoError(t, err)
		assert.Equal(t, 1, attempts)
	})

	t.Run("should retry on failure and eventually succeed", func(t *testing.T) {
		attempts := 0
		err := utils.Retry(3, time.Millisecond, func() error {
			attempts++
			if attempts < 3 {
				return assert.AnError
			}
			return nil
		})

		assert.NoError(t, err)
		assert.Equal(t, 3, attempts)
	})

	t.Run("should fail after max attempts", func(t *testing.T) {
		attempts := 0
		err := utils.Retry(3, time.Millisecond, func() error {
			attempts++
			return assert.AnError
		})

		assert.Error(t, err)
		assert.Equal(t, 3, attempts)
	})
}
