package utils

import (
	"context"
	"net"
	"net/url"
	"testing"
	"time"

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

	"github.com/pocket-id/pocket-id/backend/internal/common"
)

func TestIsLocalhostIP(t *testing.T) {
	tests := []struct {
		ip       string
		expected bool
	}{
		{"127.0.0.1", true},
		{"127.255.255.255", true},
		{"::1", true},
		{"192.168.1.1", false},
	}

	for _, tt := range tests {
		ip := net.ParseIP(tt.ip)
		got := IsLocalhostIP(ip)
		assert.Equal(t, tt.expected, got)
	}
}

func TestIsPrivateLanIP(t *testing.T) {
	tests := []struct {
		ip       string
		expected bool
	}{
		{"10.0.0.1", true},
		{"172.16.5.4", true},
		{"192.168.100.200", true},
		{"8.8.8.8", false},
		{"::1", false}, // IPv6 should return false
	}

	for _, tt := range tests {
		ip := net.ParseIP(tt.ip)
		got := IsPrivateLanIP(ip)
		assert.Equal(t, tt.expected, got)
	}
}

func TestIsTailscaleIP(t *testing.T) {
	tests := []struct {
		ip       string
		expected bool
	}{
		{"100.64.0.1", true},
		{"100.127.255.254", true},
		{"8.8.8.8", false},
		{"::1", false}, // IPv6 should return false
	}

	for _, tt := range tests {
		ip := net.ParseIP(tt.ip)

		got := IsTailscaleIP(ip)
		assert.Equal(t, tt.expected, got)
	}
}

func TestIsLocalIPv6(t *testing.T) {
	// Save and restore env config
	origRanges := common.EnvConfig.LocalIPv6Ranges
	defer func() { common.EnvConfig.LocalIPv6Ranges = origRanges }()

	common.EnvConfig.LocalIPv6Ranges = "fd00::/8,fc00::/7"
	localIPv6Ranges = nil // reset
	loadLocalIPv6Ranges()

	tests := []struct {
		ip       string
		expected bool
	}{
		{"fd00::1", true},
		{"fc00::abcd", true},
		{"::1", false},         // loopback handled separately
		{"192.168.1.1", false}, // IPv4 should return false
	}

	for _, tt := range tests {
		ip := net.ParseIP(tt.ip)
		got := IsLocalIPv6(ip)
		assert.Equal(t, tt.expected, got)
	}
}

func TestIsPrivateIP(t *testing.T) {
	// Save and restore env config
	origRanges := common.EnvConfig.LocalIPv6Ranges
	t.Cleanup(func() {
		common.EnvConfig.LocalIPv6Ranges = origRanges
	})

	common.EnvConfig.LocalIPv6Ranges = "fd00::/8"
	localIPv6Ranges = nil // reset
	loadLocalIPv6Ranges()

	tests := []struct {
		ip       string
		expected bool
	}{
		{"127.0.0.1", true},              // localhost
		{"192.168.1.1", true},            // private LAN
		{"100.64.0.1", true},             // Tailscale
		{"169.254.169.254", true},        // IPv4 link-local
		{"169.254.170.2", true},          // IPv4 link-local
		{"::ffff:169.254.169.254", true}, // IPv4-mapped link-local
		{"fe80::1", true},                // IPv6 link-local
		{"ff02::1", true},                // IPv6 link-local multicast
		{"0.0.0.0", true},                // IPv4 unspecified
		{"::", true},                     // IPv6 unspecified
		{"fd00::1", true},                // private IPv6
		{"8.8.8.8", false},               // public IPv4
		{"2001:4860:4860::8888", false},  // public IPv6
	}

	for _, tt := range tests {
		ip := net.ParseIP(tt.ip)
		got := IsPrivateIP(ip)
		assert.Equal(t, tt.expected, got)
	}
}

func TestListContainsIP(t *testing.T) {
	_, ipNet1, _ := net.ParseCIDR("10.0.0.0/8")
	_, ipNet2, _ := net.ParseCIDR("192.168.0.0/16")

	list := []*net.IPNet{ipNet1, ipNet2}

	tests := []struct {
		ip       string
		expected bool
	}{
		{"10.1.1.1", true},
		{"192.168.5.5", true},
		{"172.16.0.1", false},
	}

	for _, tt := range tests {
		ip := net.ParseIP(tt.ip)
		got := listContainsIP(list, ip)
		assert.Equal(t, tt.expected, got)
	}
}

func TestInit_LocalIPv6Ranges(t *testing.T) {
	// Save and restore env config
	origRanges := common.EnvConfig.LocalIPv6Ranges
	t.Cleanup(func() {
		common.EnvConfig.LocalIPv6Ranges = origRanges
	})

	common.EnvConfig.LocalIPv6Ranges = "fd00::/8, invalidCIDR ,fc00::/7"
	localIPv6Ranges = nil
	loadLocalIPv6Ranges()

	assert.Len(t, localIPv6Ranges, 2)
}

func TestIsURLPrivate(t *testing.T) {
	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
	defer cancel()

	tests := []struct {
		name        string
		urlStr      string
		expectPriv  bool
		expectError bool
	}{
		{
			name:        "localhost by name",
			urlStr:      "http://localhost",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "localhost with port",
			urlStr:      "http://localhost:8080",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "127.0.0.1 IP",
			urlStr:      "http://127.0.0.1",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "127.0.0.1 with port",
			urlStr:      "http://127.0.0.1:3000",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "IPv6 loopback",
			urlStr:      "http://[::1]",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "IPv6 loopback with port",
			urlStr:      "http://[::1]:8080",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "private IP 10.x.x.x",
			urlStr:      "http://10.0.0.1",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "private IP 192.168.x.x",
			urlStr:      "http://192.168.1.1",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "private IP 172.16.x.x",
			urlStr:      "http://172.16.0.1",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "Tailscale IP",
			urlStr:      "http://100.64.0.1",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "IPv4 link-local metadata IP",
			urlStr:      "http://169.254.169.254",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "IPv4-mapped link-local metadata IP",
			urlStr:      "http://[::ffff:169.254.169.254]",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "IPv6 link-local IP",
			urlStr:      "http://[fe80::1]",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "IPv4 unspecified IP",
			urlStr:      "http://0.0.0.0",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "IPv6 unspecified IP",
			urlStr:      "http://[::]",
			expectPriv:  true,
			expectError: false,
		},
		{
			name:        "public IP - Google DNS",
			urlStr:      "http://8.8.8.8",
			expectPriv:  false,
			expectError: false,
		},
		{
			name:        "public IP - Cloudflare DNS",
			urlStr:      "http://1.1.1.1",
			expectPriv:  false,
			expectError: false,
		},
		{
			name:        "invalid hostname",
			urlStr:      "http://this-should-not-resolve-ever-123456789.invalid",
			expectPriv:  false,
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			u, err := url.Parse(tt.urlStr)
			require.NoError(t, err, "Failed to parse URL %s", tt.urlStr)

			isPriv, err := IsURLPrivate(ctx, u)

			if tt.expectError {
				require.Error(t, err, "IsURLPrivate(%s) expected error but got none", tt.urlStr)
			} else {
				require.NoError(t, err, "IsURLPrivate(%s) unexpected error", tt.urlStr)
				assert.Equal(t, tt.expectPriv, isPriv, "IsURLPrivate(%s)", tt.urlStr)
			}
		})
	}
}

func TestIsURLPrivate_WithDomainName(t *testing.T) {
	// Note: These tests rely on actual DNS resolution
	// They test real public domains to ensure they are not flagged as private
	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
	defer cancel()

	tests := []struct {
		name       string
		urlStr     string
		expectPriv bool
	}{
		{
			name:       "Google public domain",
			urlStr:     "https://www.google.com",
			expectPriv: false,
		},
		{
			name:       "GitHub public domain",
			urlStr:     "https://github.com",
			expectPriv: false,
		},
		{
			// localhost.localtest.me is a well-known domain that resolves to 127.0.0.1
			name:       "localhost.localtest.me resolves to 127.0.0.1",
			urlStr:     "http://localhost.localtest.me",
			expectPriv: true,
		},
		{
			// 10.0.0.1.nip.io resolves to 10.0.0.1 (private IP)
			name:       "nip.io domain resolving to private 10.x IP",
			urlStr:     "http://10.0.0.1.nip.io",
			expectPriv: true,
		},
		{
			// 192.168.1.1.nip.io resolves to 192.168.1.1 (private IP)
			name:       "nip.io domain resolving to private 192.168.x IP",
			urlStr:     "http://192.168.1.1.nip.io",
			expectPriv: true,
		},
		{
			// 127.0.0.1.nip.io resolves to 127.0.0.1 (localhost)
			name:       "nip.io domain resolving to localhost",
			urlStr:     "http://127.0.0.1.nip.io",
			expectPriv: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			u, err := url.Parse(tt.urlStr)
			require.NoError(t, err, "Failed to parse URL %s", tt.urlStr)

			isPriv, err := IsURLPrivate(ctx, u)
			if err != nil {
				t.Skipf("DNS resolution failed for %s (network issue?): %v", tt.urlStr, err)
				return
			}

			assert.Equal(t, tt.expectPriv, isPriv, "IsURLPrivate(%s)", tt.urlStr)
		})
	}
}

func TestIsURLPrivate_ContextCancellation(t *testing.T) {
	ctx, cancel := context.WithCancel(t.Context())
	cancel() // Cancel immediately

	u, err := url.Parse("http://example.com")
	require.NoError(t, err, "Failed to parse URL")

	_, err = IsURLPrivate(ctx, u)
	assert.Error(t, err, "IsURLPrivate with cancelled context expected error but got none")
}
