package api

import (
	"net/netip"
	"testing"

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

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

	tests := []struct {
		name    string
		spec    PortSpec
		wantErr string
	}{
		// Valid ingress mode.
		{
			name: "ingress mode tcp",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode with published tcp port",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode udp",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode with published udp port",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode without hostname http",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode without hostname https",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode with hostname and http",
			spec: PortSpec{
				Hostname:      "app.example.com",
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode with hostname and https",
			spec: PortSpec{
				Hostname:      "app.example.com",
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "ingress mode with hostname and published port",
			spec: PortSpec{
				Hostname:      "app.example.com",
				PublishedPort: 6443,
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
		},

		// Valid host mode.
		{
			name: "host mode tcp",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeHost,
			},
		},
		{
			name: "host mode udp",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeHost,
			},
		},
		{
			name: "host mode with IPv4",
			spec: PortSpec{
				HostIP:        netip.MustParseAddr("127.0.0.1"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
		},
		{
			name: "host mode with IPv6",
			spec: PortSpec{
				HostIP:        netip.MustParseAddr("2001:db8::1234:5678"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeHost,
			},
		},

		// Error cases.
		{
			name: "missing container port",
			spec: PortSpec{
				Protocol: ProtocolTCP,
			},
			wantErr: "container port must be non-zero",
		},
		{
			name: "invalid protocol",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      "invalid",
			},
			wantErr: "invalid protocol 'invalid'",
		},
		{
			name: "invalid mode",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          "invalid",
			},
			wantErr: "invalid mode: 'invalid'",
		},
		{
			name: "hostname with non-http protocol",
			spec: PortSpec{
				Hostname:      "app.example.com",
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
			wantErr: "hostname is only valid with 'http' or 'https' protocols",
		},
		{
			name: "invalid hostname",
			spec: PortSpec{
				Hostname:      "app",
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
			wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
		},
		{
			name: "host IP in ingress mode",
			spec: PortSpec{
				HostIP:        netip.MustParseAddr("127.0.0.1"),
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
			wantErr: "host IP cannot be specified in ingress mode",
		},
		{
			name: "zero published port in host mode",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
			wantErr: "published port is required in host mode",
		},
		{
			name: "hostname in host mode",
			spec: PortSpec{
				Hostname:      "app.example.com",
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
			wantErr: "hostname cannot be specified in host mode",
		},
		{
			name: "http in host mode",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeHost,
			},
			wantErr: "unsupported protocol 'http' in host mode",
		},
		{
			name: "https in host mode",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeHost,
			},
			wantErr: "unsupported protocol 'https' in host mode",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			err := tt.spec.Validate()
			if tt.wantErr != "" {
				require.Error(t, err, tt.wantErr)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}
			require.NoError(t, err)
		})
	}
}

func TestPortSpec_String(t *testing.T) {
	tests := []struct {
		name     string
		spec     PortSpec
		expected string
	}{
		// Ingress mode.
		{
			name: "container port only",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
			expected: "8080/tcp",
		},
		{
			name: "container port udp",
			spec: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeIngress,
			},
			expected: "8080/udp",
		},
		{
			name: "published and container port",
			spec: PortSpec{
				ContainerPort: 8080,
				PublishedPort: 80,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
			expected: "80:8080/tcp",
		},
		{
			name: "hostname and container port https",
			spec: PortSpec{
				Hostname:      "app.example.com",
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
			expected: "app.example.com:8080/https",
		},
		{
			name: "hostname and container port http",
			spec: PortSpec{
				Hostname:      "app.example.com",
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeIngress,
			},
			expected: "app.example.com:8080/http",
		},
		{
			name: "hostname and published and container port https",
			spec: PortSpec{
				Hostname:      "app.example.com",
				PublishedPort: 6443,
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
			expected: "app.example.com:6443:8080/https",
		},
		{
			name: "hostname and published and container port http",
			spec: PortSpec{
				Hostname:      "app.example.com",
				PublishedPort: 6443,
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeIngress,
			},
			expected: "app.example.com:6443:8080/http",
		},

		// Host mode.
		{
			name: "host mode tcp",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
			expected: "80:8080/tcp@host",
		},
		{
			name: "host mode udp",
			spec: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeHost,
			},
			expected: "80:8080/udp@host",
		},
		{
			name: "host mode with IPv4 tcp",
			spec: PortSpec{
				HostIP:        netip.MustParseAddr("127.0.0.1"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
			expected: "127.0.0.1:80:8080/tcp@host",
		},
		{
			name: "host mode with IPv4 udp",
			spec: PortSpec{
				HostIP:        netip.MustParseAddr("127.0.0.1"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeHost,
			},
			expected: "127.0.0.1:80:8080/udp@host",
		},
		{
			name: "host mode with IPv6",
			spec: PortSpec{
				HostIP:        netip.MustParseAddr("2001:db8::1234:5678"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
			expected: "[2001:db8::1234:5678]:80:8080/tcp@host",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := tt.spec.String()
			require.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

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

	tests := []struct {
		name     string
		port     string
		expected PortSpec
		wantErr  string
	}{
		// Ingress mode (default).
		{
			name: "container port only",
			port: "8080",
			expected: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "published and container port",
			port: "8000:8080",
			expected: PortSpec{
				PublishedPort: 8000,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "published port zero",
			port: "0:8080",
			expected: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "tcp protocol explicit",
			port: "8080/tcp",
			expected: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "udp protocol",
			port: "8080/udp",
			expected: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "published port udp",
			port: "8000:8080/udp",
			expected: PortSpec{
				PublishedPort: 8000,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "hostname and container port",
			port: "app.example.com:8080",
			expected: PortSpec{
				Hostname:      "app.example.com",
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "container port http without hostname",
			port: "8080/http",
			expected: PortSpec{
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "hostname and container port http",
			port: "app.example.com:8080/http",
			expected: PortSpec{
				Hostname:      "app.example.com",
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "hostname and published port",
			port: "app.example.com:6443:8080",
			expected: PortSpec{
				Hostname:      "app.example.com",
				PublishedPort: 6443,
				ContainerPort: 8080,
				Protocol:      ProtocolHTTPS,
				Mode:          PortModeIngress,
			},
		},
		{
			name: "hostname and published port http",
			port: "app.example.com:8000:8080/http",
			expected: PortSpec{
				Hostname:      "app.example.com",
				PublishedPort: 8000,
				ContainerPort: 8080,
				Protocol:      ProtocolHTTP,
				Mode:          PortModeIngress,
			},
		},

		// Host mode.
		{
			name: "host mode published with protocol",
			port: "80:8080/udp@host",
			expected: PortSpec{
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeHost,
			},
		},
		{
			name: "host mode with IPv4",
			port: "127.0.0.1:80:8080@host",
			expected: PortSpec{
				HostIP:        netip.MustParseAddr("127.0.0.1"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
		},
		{
			name: "host mode with IPv6",
			port: "[2001:db8::1234:5678]:80:8080@host",
			expected: PortSpec{
				HostIP:        netip.MustParseAddr("2001:db8::1234:5678"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolTCP,
				Mode:          PortModeHost,
			},
		},
		{
			name: "host mode with IP and protocol",
			port: "127.0.0.1:80:8080/udp@host",
			expected: PortSpec{
				HostIP:        netip.MustParseAddr("127.0.0.1"),
				PublishedPort: 80,
				ContainerPort: 8080,
				Protocol:      ProtocolUDP,
				Mode:          PortModeHost,
			},
		},

		// Error cases.
		{
			name:    "empty",
			port:    "",
			wantErr: "invalid container port",
		},
		{
			name:    "invalid container port",
			port:    "invalid",
			wantErr: "invalid container port",
		},
		{
			name:    "container port zero",
			port:    "0",
			wantErr: "container port must be non-zero",
		},
		{
			name:    "out of range container port",
			port:    "100500",
			wantErr: "invalid container port",
		},
		{
			name:    "just protocol",
			port:    "/tcp",
			wantErr: "invalid container port",
		},
		{
			name:    "just mode",
			port:    "@host",
			wantErr: "invalid container port",
		},
		{
			name:    "multiple @ symbols",
			port:    "8080@host@host",
			wantErr: "too many '@' symbols",
		},
		{
			name:    "invalid mode",
			port:    "8080@invalid",
			wantErr: "invalid mode: 'invalid'",
		},
		{
			name:    "multiple protocols",
			port:    "8080/tcp/udp",
			wantErr: "too many '/' symbols",
		},
		{
			name:    "invalid protocol",
			port:    "8080/invalid",
			wantErr: "unsupported protocol: 'invalid'",
		},
		{
			name:    "invalid published port",
			port:    "app.example.com:invalid:8080",
			wantErr: "invalid published port",
		},
		{
			name:    "invalid hostname",
			port:    "app:8080/http",
			wantErr: "invalid hostname 'app': must be a valid domain name containing at least one dot",
		},
		{
			name:    "hostname with tcp protocol",
			port:    "app.example.com:8080/tcp",
			wantErr: "hostname is only valid with 'http' or 'https' protocols",
		},

		{
			name:    "missing published port in host mode",
			port:    "8080@host",
			wantErr: "published port is required in host mode",
		},
		{
			name:    "missing published port with protocol in host mode",
			port:    "8080/udp@host",
			wantErr: "published port is required in host mode",
		},
		{
			name:    "invalid host IPv4",
			port:    "300.0.0.1:80:8080@host",
			wantErr: "invalid host IP",
		},
		{
			name:    "invalid host IPv6",
			port:    "[:::1]:80:8080@host",
			wantErr: "invalid host IP",
		},
		{
			name:    "missing closing bracket in IPv6",
			port:    "[::1:80:8080@host",
			wantErr: "invalid host IP",
		},
		{
			name:    "missing brackets in IPv6",
			port:    "2001:db8::1234:5678:80:8080@host",
			wantErr: "invalid host IP",
		},
		{
			name:    "http in host mode",
			port:    "80:8080/http@host",
			wantErr: "unsupported protocol 'http' in host mode, only 'tcp' and 'udp' are supported",
		},
		{
			name:    "https in host mode",
			port:    "80:8080/https@host",
			wantErr: "unsupported protocol 'https' in host mode, only 'tcp' and 'udp' are supported",
		},
		{
			name:    "hostname in host mode",
			port:    "app.example.com:8080@host",
			wantErr: "hostname cannot be specified in host mode",
		},
		{
			name:    "hostname with invalid published port",
			port:    "app.example.com:invalid:8080@host",
			wantErr: "invalid published port",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			spec, err := ParsePortSpec(tt.port)
			if tt.wantErr != "" {
				require.Error(t, err, "Expected error: %s, got nil, spec: %+v", tt.wantErr, spec)
				assert.Contains(t, err.Error(), tt.wantErr)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.expected, spec)
		})
	}
}
