package opts

import (
	"testing"

	"github.com/docker/docker/api/types/swarm"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

func TestPortOptValidSimpleSyntax(t *testing.T) {
	testCases := []struct {
		value    string
		expected []swarm.PortConfig
	}{
		{
			value: "80",
			expected: []swarm.PortConfig{
				{
					Protocol:    "tcp",
					TargetPort:  80,
					PublishMode: swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "80:8080",
			expected: []swarm.PortConfig{
				{
					Protocol:      "tcp",
					TargetPort:    8080,
					PublishedPort: 80,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "8080:80/tcp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "tcp",
					TargetPort:    80,
					PublishedPort: 8080,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "80:8080/udp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "udp",
					TargetPort:    8080,
					PublishedPort: 80,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "80-81:8080-8081/tcp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "tcp",
					TargetPort:    8080,
					PublishedPort: 80,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
				{
					Protocol:      "tcp",
					TargetPort:    8081,
					PublishedPort: 81,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "80-82:8080-8082/udp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "udp",
					TargetPort:    8080,
					PublishedPort: 80,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
				{
					Protocol:      "udp",
					TargetPort:    8081,
					PublishedPort: 81,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
				{
					Protocol:      "udp",
					TargetPort:    8082,
					PublishedPort: 82,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "80-82:8080/udp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "udp",
					TargetPort:    8080,
					PublishedPort: 80,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
				{
					Protocol:      "udp",
					TargetPort:    8080,
					PublishedPort: 81,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
				{
					Protocol:      "udp",
					TargetPort:    8080,
					PublishedPort: 82,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "80-80:8080/tcp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "tcp",
					TargetPort:    8080,
					PublishedPort: 80,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
	}
	for _, tc := range testCases {
		var port PortOpt
		assert.NilError(t, port.Set(tc.value))
		assert.Check(t, is.Len(port.Value(), len(tc.expected)))
		for _, expectedPortConfig := range tc.expected {
			assertContains(t, port.Value(), expectedPortConfig)
		}
	}
}

func TestPortOptValidComplexSyntax(t *testing.T) {
	testCases := []struct {
		value    string
		expected []swarm.PortConfig
	}{
		{
			value: "target=80",
			expected: []swarm.PortConfig{
				{
					TargetPort:  80,
					Protocol:    "tcp",
					PublishMode: swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "target=80,protocol=tcp",
			expected: []swarm.PortConfig{
				{
					Protocol:    "tcp",
					TargetPort:  80,
					PublishMode: swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "target=80,published=8080,protocol=tcp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "tcp",
					TargetPort:    80,
					PublishedPort: 8080,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "published=80,target=8080,protocol=tcp",
			expected: []swarm.PortConfig{
				{
					Protocol:      "tcp",
					TargetPort:    8080,
					PublishedPort: 80,
					PublishMode:   swarm.PortConfigPublishModeIngress,
				},
			},
		},
		{
			value: "target=80,published=8080,protocol=tcp,mode=host",
			expected: []swarm.PortConfig{
				{
					Protocol:      "tcp",
					TargetPort:    80,
					PublishedPort: 8080,
					PublishMode:   "host",
				},
			},
		},
		{
			value: "target=80,published=8080,mode=host",
			expected: []swarm.PortConfig{
				{
					TargetPort:    80,
					PublishedPort: 8080,
					PublishMode:   "host",
					Protocol:      "tcp",
				},
			},
		},
		{
			value: "target=80,published=8080,mode=ingress",
			expected: []swarm.PortConfig{
				{
					TargetPort:    80,
					PublishedPort: 8080,
					PublishMode:   "ingress",
					Protocol:      "tcp",
				},
			},
		},
	}
	for _, tc := range testCases {
		var port PortOpt
		assert.NilError(t, port.Set(tc.value))
		assert.Check(t, is.Len(port.Value(), len(tc.expected)))
		for _, expectedPortConfig := range tc.expected {
			assertContains(t, port.Value(), expectedPortConfig)
		}
	}
}

func TestPortOptInvalidComplexSyntax(t *testing.T) {
	testCases := []struct {
		value         string
		expectedError string
	}{
		{
			value:         "invalid,target=80",
			expectedError: "invalid field",
		},
		{
			value:         "invalid=field",
			expectedError: "invalid field",
		},
		{
			value:         "protocol=invalid",
			expectedError: "invalid protocol value",
		},
		{
			value:         "target=invalid",
			expectedError: "invalid syntax",
		},
		{
			value:         "published=invalid",
			expectedError: "invalid syntax",
		},
		{
			value:         "mode=invalid",
			expectedError: "invalid publish mode value",
		},
		{
			value:         "published=8080,protocol=tcp,mode=ingress",
			expectedError: "missing mandatory field",
		},
		{
			value:         `target=80,protocol="tcp,mode=ingress"`,
			expectedError: "non-quoted-field",
		},
		{
			value:         `target=80,"protocol=tcp,mode=ingress"`,
			expectedError: "invalid protocol value",
		},
	}
	for _, tc := range testCases {
		var port PortOpt
		assert.ErrorContains(t, port.Set(tc.value), tc.expectedError)
	}
}

func TestPortOptInvalidSimpleSyntax(t *testing.T) {
	testCases := []struct {
		value         string
		expectedError string
	}{
		{
			value:         "9999999",
			expectedError: "Invalid containerPort: 9999999",
		},
		{
			value:         "80/xyz",
			expectedError: "Invalid proto: xyz",
		},
		{
			value:         "tcp",
			expectedError: "Invalid containerPort: tcp",
		},
		{
			value:         "udp",
			expectedError: "Invalid containerPort: udp",
		},
		{
			value:         "",
			expectedError: "No port specified: <empty>",
		},
		{
			value:         "1.1.1.1:80:80",
			expectedError: "hostip is not supported",
		},
	}
	for _, tc := range testCases {
		var port PortOpt
		assert.Error(t, port.Set(tc.value), tc.expectedError)
	}
}

func assertContains(t *testing.T, portConfigs []swarm.PortConfig, expected swarm.PortConfig) {
	var contains = false
	for _, portConfig := range portConfigs {
		if portConfig == expected {
			contains = true
			break
		}
	}
	if !contains {
		t.Errorf("expected %v to contain %v, did not", portConfigs, expected)
	}
}
