package marathon

import (
	"testing"

	"github.com/containous/traefik/provider/label"
	"github.com/containous/traefik/types"
	"github.com/gambol99/go-marathon"
	"github.com/stretchr/testify/assert"
)

func TestGetConfigurationAPIErrorsV1(t *testing.T) {
	fakeClient := newFakeClient(true, marathon.Applications{})

	p := &Provider{
		marathonClient: fakeClient,
	}
	p.TemplateVersion = 1

	actualConfig := p.getConfiguration()
	fakeClient.AssertExpectations(t)

	if actualConfig != nil {
		t.Errorf("configuration should have been nil, got %v", actualConfig)
	}
}

func TestBuildConfigurationV1(t *testing.T) {
	testCases := []struct {
		desc              string
		application       marathon.Application
		expectedFrontends map[string]*types.Frontend
		expectedBackends  map[string]*types.Backend
	}{
		{
			desc: "simple application",
			application: application(
				appPorts(80),
				withTasks(localhostTask(taskPorts(80))),
			),
			expectedFrontends: map[string]*types.Frontend{
				"frontend-app": {
					Backend: "backend-app",
					Routes: map[string]types.Route{
						"route-host-app": {
							Rule: "Host:app.marathon.localhost",
						},
					},
					PassHostHeader: true,
					BasicAuth:      []string{},
					EntryPoints:    []string{},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backend-app": {
					Servers: map[string]types.Server{
						"server-task": {
							URL:    "http://localhost:80",
							Weight: label.DefaultWeight,
						},
					},
					CircuitBreaker: nil,
				},
			},
		},
		{
			desc: "filtered task",
			application: application(
				appPorts(80),
				withTasks(localhostTask(taskPorts(80), taskState(taskStateStaging))),
			),
			expectedFrontends: map[string]*types.Frontend{
				"frontend-app": {
					Backend: "backend-app",
					Routes: map[string]types.Route{
						"route-host-app": {
							Rule: "Host:app.marathon.localhost",
						},
					},
					PassHostHeader: true,
					BasicAuth:      []string{},
					EntryPoints:    []string{},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backend-app": {},
			},
		},
		{
			desc: "max connection extractor function label only",
			application: application(
				appPorts(80),
				withTasks(localhostTask(taskPorts(80))),

				withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"),
			),
			expectedFrontends: map[string]*types.Frontend{
				"frontend-app": {
					Backend: "backend-app",
					Routes: map[string]types.Route{
						"route-host-app": {
							Rule: "Host:app.marathon.localhost",
						},
					},
					PassHostHeader: true,
					BasicAuth:      []string{},
					EntryPoints:    []string{},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backend-app": {
					Servers: map[string]types.Server{
						"server-task": {
							URL:    "http://localhost:80",
							Weight: label.DefaultWeight,
						},
					},
					MaxConn: nil,
				},
			},
		},
		{
			desc: "multiple ports",
			application: application(
				appPorts(80, 81),
				withTasks(localhostTask(taskPorts(80, 81))),
			),
			expectedFrontends: map[string]*types.Frontend{
				"frontend-app": {
					Backend: "backend-app",
					Routes: map[string]types.Route{
						"route-host-app": {
							Rule: "Host:app.marathon.localhost",
						},
					},
					PassHostHeader: true,
					BasicAuth:      []string{},
					EntryPoints:    []string{},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backend-app": {
					Servers: map[string]types.Server{
						"server-task": {
							URL:    "http://localhost:80",
							Weight: label.DefaultWeight,
						},
					},
				},
			},
		},
		{
			desc: "with all labels",
			application: application(
				appPorts(80),
				withTasks(task(host("127.0.0.1"), taskPorts(80))),

				withLabel(label.TraefikPort, "666"),
				withLabel(label.TraefikProtocol, "https"),
				withLabel(label.TraefikWeight, "12"),

				withLabel(label.TraefikBackend, "foobar"),

				withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"),
				withLabel(label.TraefikBackendHealthCheckPath, "/health"),
				withLabel(label.TraefikBackendHealthCheckInterval, "6"),
				withLabel(label.TraefikBackendLoadBalancerMethod, "drr"),
				withLabel(label.TraefikBackendLoadBalancerSticky, "true"),
				withLabel(label.TraefikBackendLoadBalancerStickiness, "true"),
				withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"),
				withLabel(label.TraefikBackendMaxConnAmount, "666"),
				withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"),

				withLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"),
				withLabel(label.TraefikFrontendEntryPoints, "http,https"),
				withLabel(label.TraefikFrontendPassHostHeader, "true"),
				withLabel(label.TraefikFrontendPriority, "666"),
				withLabel(label.TraefikFrontendRule, "Host:traefik.io"),
			),
			expectedFrontends: map[string]*types.Frontend{
				"frontend-app": {
					EntryPoints: []string{
						"http",
						"https",
					},
					Backend: "backendfoobar",
					Routes: map[string]types.Route{
						"route-host-app": {
							Rule: "Host:traefik.io",
						},
					},
					PassHostHeader: true,
					Priority:       666,
					BasicAuth: []string{
						"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
						"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
					},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backendfoobar": {
					Servers: map[string]types.Server{
						"server-task": {
							URL:    "https://127.0.0.1:666",
							Weight: 12,
						},
					},
					CircuitBreaker: &types.CircuitBreaker{
						Expression: "NetworkErrorRatio() > 0.5",
					},
					LoadBalancer: &types.LoadBalancer{
						Method: "drr",
						Sticky: true,
						Stickiness: &types.Stickiness{
							CookieName: "chocolate",
						},
					},
					MaxConn: &types.MaxConn{
						Amount:        666,
						ExtractorFunc: "client.ip",
					},
					HealthCheck: &types.HealthCheck{
						Path:     "/health",
						Interval: "6",
					},
				},
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			test.application.ID = "/app"

			for _, task := range test.application.Tasks {
				task.ID = "task"
				if task.State == "" {
					task.State = "TASK_RUNNING"
				}
			}

			p := &Provider{
				Domain:           "marathon.localhost",
				ExposedByDefault: true,
			}

			actualConfig := p.buildConfigurationV1(withApplications(test.application))

			assert.NotNil(t, actualConfig)
			assert.Equal(t, test.expectedBackends, actualConfig.Backends)
			assert.Equal(t, test.expectedFrontends, actualConfig.Frontends)
		})
	}
}

func TestBuildConfigurationServicesV1(t *testing.T) {
	testCases := []struct {
		desc              string
		application       marathon.Application
		expectedFrontends map[string]*types.Frontend
		expectedBackends  map[string]*types.Backend
	}{
		{
			desc: "multiple ports with services",
			application: application(
				appPorts(80, 81),
				withTasks(localhostTask(taskPorts(80, 81))),

				withLabel(label.TraefikBackendMaxConnAmount, "1000"),
				withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"),
				withSegmentLabel(label.TraefikPort, "80", "web"),
				withSegmentLabel(label.TraefikPort, "81", "admin"),
				withLabel("traefik..port", "82"), // This should be ignored, as it fails to match the servicesPropertiesRegexp regex.
				withSegmentLabel(label.TraefikFrontendRule, "Host:web.app.marathon.localhost", "web"),
				withSegmentLabel(label.TraefikFrontendRule, "Host:admin.app.marathon.localhost", "admin"),
			),
			expectedFrontends: map[string]*types.Frontend{
				"frontend-app-service-web": {
					Backend: "backend-app-service-web",
					Routes: map[string]types.Route{
						`route-host-app-service-web`: {
							Rule: "Host:web.app.marathon.localhost",
						},
					},
					PassHostHeader: true,
					BasicAuth:      []string{},
					EntryPoints:    []string{},
				},
				"frontend-app-service-admin": {
					Backend: "backend-app-service-admin",
					Routes: map[string]types.Route{
						`route-host-app-service-admin`: {
							Rule: "Host:admin.app.marathon.localhost",
						},
					},
					PassHostHeader: true,
					BasicAuth:      []string{},
					EntryPoints:    []string{},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backend-app-service-web": {
					Servers: map[string]types.Server{
						"server-task-service-web": {
							URL:    "http://localhost:80",
							Weight: label.DefaultWeight,
						},
					},
					MaxConn: &types.MaxConn{
						Amount:        1000,
						ExtractorFunc: "client.ip",
					},
				},
				"backend-app-service-admin": {
					Servers: map[string]types.Server{
						"server-task-service-admin": {
							URL:    "http://localhost:81",
							Weight: label.DefaultWeight,
						},
					},
					MaxConn: &types.MaxConn{
						Amount:        1000,
						ExtractorFunc: "client.ip",
					},
				},
			},
		},
		{
			desc: "when all labels are set",
			application: application(
				appPorts(80, 81),
				withTasks(localhostTask(taskPorts(80, 81))),

				withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"),
				withLabel(label.TraefikBackendHealthCheckPath, "/health"),
				withLabel(label.TraefikBackendHealthCheckInterval, "6"),
				withLabel(label.TraefikBackendLoadBalancerMethod, "drr"),
				withLabel(label.TraefikBackendLoadBalancerSticky, "true"),
				withLabel(label.TraefikBackendLoadBalancerStickiness, "true"),
				withLabel(label.TraefikBackendLoadBalancerStickinessCookieName, "chocolate"),
				withLabel(label.TraefikBackendMaxConnAmount, "666"),
				withLabel(label.TraefikBackendMaxConnExtractorFunc, "client.ip"),

				withSegmentLabel(label.TraefikPort, "80", "containous"),
				withSegmentLabel(label.TraefikProtocol, "https", "containous"),
				withSegmentLabel(label.TraefikWeight, "12", "containous"),

				withSegmentLabel(label.TraefikFrontendAuthBasic, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "containous"),
				withSegmentLabel(label.TraefikFrontendEntryPoints, "http,https", "containous"),
				withSegmentLabel(label.TraefikFrontendPassHostHeader, "true", "containous"),
				withSegmentLabel(label.TraefikFrontendPriority, "666", "containous"),
				withSegmentLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"),
			),
			expectedFrontends: map[string]*types.Frontend{
				"frontend-app-service-containous": {
					EntryPoints: []string{
						"http",
						"https",
					},
					Backend: "backend-app-service-containous",
					Routes: map[string]types.Route{
						"route-host-app-service-containous": {
							Rule: "Host:traefik.io",
						},
					},
					PassHostHeader: true,
					Priority:       666,
					BasicAuth: []string{
						"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
						"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
					},
				},
			},
			expectedBackends: map[string]*types.Backend{
				"backend-app-service-containous": {
					Servers: map[string]types.Server{
						"server-task-service-containous": {
							URL:    "https://localhost:80",
							Weight: 12,
						},
					},
					CircuitBreaker: &types.CircuitBreaker{
						Expression: "NetworkErrorRatio() > 0.5",
					},
					LoadBalancer: &types.LoadBalancer{
						Method: "drr",
						Sticky: true,
						Stickiness: &types.Stickiness{
							CookieName: "chocolate",
						},
					},
					MaxConn: &types.MaxConn{
						Amount:        666,
						ExtractorFunc: "client.ip",
					},
					HealthCheck: &types.HealthCheck{
						Path:     "/health",
						Interval: "6",
					},
				},
			},
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			test.application.ID = "/app"

			for _, task := range test.application.Tasks {
				task.ID = "task"
				if task.State == "" {
					task.State = "TASK_RUNNING"
				}
			}

			p := &Provider{
				Domain:           "marathon.localhost",
				ExposedByDefault: true,
			}

			actualConfig := p.buildConfigurationV1(withApplications(test.application))

			assert.NotNil(t, actualConfig)
			assert.Equal(t, test.expectedBackends, actualConfig.Backends)
			assert.Equal(t, test.expectedFrontends, actualConfig.Frontends)
		})
	}
}

func TestGetPortV1(t *testing.T) {
	testCases := []struct {
		desc        string
		application marathon.Application
		task        marathon.Task
		serviceName string
		expected    string
	}{
		{
			desc:        "port missing",
			application: application(),
			task:        task(),
			expected:    "",
		},
		{
			desc:        "numeric port",
			application: application(withLabel(label.TraefikPort, "80")),
			task:        task(),
			expected:    "80",
		},
		{
			desc:        "string port",
			application: application(withLabel(label.TraefikPort, "foobar")),
			task:        task(taskPorts(80)),
			expected:    "",
		},
		{
			desc:        "negative port",
			application: application(withLabel(label.TraefikPort, "-1")),
			task:        task(taskPorts(80)),
			expected:    "",
		},
		{
			desc:        "task port available",
			application: application(),
			task:        task(taskPorts(80)),
			expected:    "80",
		},
		{
			desc: "port definition available",
			application: application(
				portDefinition(443),
			),
			task:     task(),
			expected: "443",
		},
		{
			desc:        "IP-per-task port available",
			application: application(ipAddrPerTask(8000)),
			task:        task(),
			expected:    "8000",
		},
		{
			desc:        "multiple task ports available",
			application: application(),
			task:        task(taskPorts(80, 443)),
			expected:    "80",
		},
		{
			desc:        "numeric port index specified",
			application: application(withLabel(label.TraefikPortIndex, "1")),
			task:        task(taskPorts(80, 443)),
			expected:    "443",
		},
		{
			desc:        "string port index specified",
			application: application(withLabel(label.TraefikPortIndex, "foobar")),
			task:        task(taskPorts(80)),
			expected:    "80",
		},
		{
			desc: "port and port index specified",
			application: application(
				withLabel(label.TraefikPort, "80"),
				withLabel(label.TraefikPortIndex, "1"),
			),
			task:     task(taskPorts(80, 443)),
			expected: "80",
		},
		{
			desc:        "task and application ports specified",
			application: application(appPorts(9999)),
			task:        task(taskPorts(7777)),
			expected:    "7777",
		},
		{
			desc:        "multiple task ports with service index available",
			application: application(withLabel(label.Prefix+"http.portIndex", "0")),
			task:        task(taskPorts(80, 443)),
			serviceName: "http",
			expected:    "80",
		},
		{
			desc:        "multiple task ports with service port available",
			application: application(withLabel(label.Prefix+"https.port", "443")),
			task:        task(taskPorts(80, 443)),
			serviceName: "https",
			expected:    "443",
		},
		{
			desc:        "multiple task ports with services but default port available",
			application: application(withLabel(label.Prefix+"http.weight", "100")),
			task:        task(taskPorts(80, 443)),
			serviceName: "http",
			expected:    "80",
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := getPortV1(test.task, test.application, test.serviceName)

			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestGetFrontendRuleV1(t *testing.T) {
	testCases := []struct {
		desc                    string
		application             marathon.Application
		serviceName             string
		expected                string
		marathonLBCompatibility bool
	}{
		{
			desc:                    "label missing",
			application:             application(appID("test")),
			marathonLBCompatibility: true,
			expected:                "Host:test.marathon.localhost",
		},
		{
			desc: "HAProxy vhost available and LB compat disabled",
			application: application(
				appID("test"),
				withLabel("HAPROXY_0_VHOST", "foo.bar"),
			),
			marathonLBCompatibility: false,
			expected:                "Host:test.marathon.localhost",
		},
		{
			desc:                    "HAProxy vhost available and LB compat enabled",
			application:             application(withLabel("HAPROXY_0_VHOST", "foo.bar")),
			marathonLBCompatibility: true,
			expected:                "Host:foo.bar",
		},
		{
			desc: "frontend rule available",

			application: application(
				withLabel(label.TraefikFrontendRule, "Host:foo.bar"),
				withLabel("HAPROXY_0_VHOST", "unused"),
			),
			marathonLBCompatibility: true,
			expected:                "Host:foo.bar",
		},
		{
			desc:                    "service label existing",
			application:             application(withSegmentLabel(label.TraefikFrontendRule, "Host:foo.bar", "app")),
			serviceName:             "app",
			marathonLBCompatibility: true,
			expected:                "Host:foo.bar",
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			p := &Provider{
				Domain:                  "marathon.localhost",
				MarathonLBCompatibility: test.marathonLBCompatibility,
			}

			actual := p.getFrontendRuleV1(test.application, test.serviceName)

			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestGetBackendV1(t *testing.T) {
	testCases := []struct {
		desc        string
		application marathon.Application
		serviceName string
		expected    string
	}{
		{
			desc:        "label missing",
			application: application(appID("/group/app")),
			expected:    "backend-group-app",
		},
		{
			desc:        "label existing",
			application: application(withLabel(label.TraefikBackend, "bar")),
			expected:    "backendbar",
		},
		{
			desc:        "service label existing",
			application: application(withSegmentLabel(label.TraefikBackend, "bar", "app")),
			serviceName: "app",
			expected:    "backendbar",
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			p := &Provider{}

			actual := p.getBackendNameV1(test.application, test.serviceName)

			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestGetBackendServerV1(t *testing.T) {
	host := "host"
	testCases := []struct {
		desc              string
		application       marathon.Application
		task              marathon.Task
		forceTaskHostname bool
		expectedServer    string
	}{
		{
			desc:           "application without IP-per-task",
			application:    application(),
			expectedServer: host,
		},
		{
			desc:              "task hostname override",
			application:       application(ipAddrPerTask(8000)),
			forceTaskHostname: true,
			expectedServer:    host,
		},
		{
			desc:           "task IP address missing",
			application:    application(ipAddrPerTask(8000)),
			task:           task(),
			expectedServer: "",
		},
		{
			desc:           "single task IP address",
			application:    application(ipAddrPerTask(8000)),
			task:           task(ipAddresses("1.1.1.1")),
			expectedServer: "1.1.1.1",
		},
		{
			desc:           "multiple task IP addresses without index label",
			application:    application(ipAddrPerTask(8000)),
			task:           task(ipAddresses("1.1.1.1", "2.2.2.2")),
			expectedServer: "",
		},
		{
			desc: "multiple task IP addresses with invalid index label",
			application: application(
				withLabel("traefik.ipAddressIdx", "invalid"),
				ipAddrPerTask(8000),
			),
			task:           task(ipAddresses("1.1.1.1", "2.2.2.2")),
			expectedServer: "",
		},
		{
			desc: "multiple task IP addresses with valid index label",
			application: application(
				withLabel("traefik.ipAddressIdx", "1"),
				ipAddrPerTask(8000),
			),
			task:           task(ipAddresses("1.1.1.1", "2.2.2.2")),
			expectedServer: "2.2.2.2",
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			p := &Provider{ForceTaskHostname: test.forceTaskHostname}
			test.task.Host = host

			actualServer := p.getBackendServerV1(test.task, test.application)

			assert.Equal(t, test.expectedServer, actualServer)
		})
	}
}

func TestGetStickyV1(t *testing.T) {
	testCases := []struct {
		desc        string
		application marathon.Application
		expected    bool
	}{
		{
			desc:        "label missing",
			application: application(),
			expected:    false,
		},
		{
			desc:        "label existing",
			application: application(withLabel(label.TraefikBackendLoadBalancerSticky, "true")),
			expected:    true,
		},
	}

	for _, test := range testCases {
		test := test
		t.Run(test.desc, func(t *testing.T) {
			t.Parallel()

			actual := getStickyV1(test.application)
			if actual != test.expected {
				t.Errorf("actual %v, expected %v", actual, test.expected)
			}
		})
	}
}

func TestGetServersV1(t *testing.T) {
	testCases := []struct {
		desc        string
		application marathon.Application
		segmentName string
		expected    map[string]types.Server
	}{
		{
			desc:        "should return nil when no task",
			application: application(ipAddrPerTask(80)),
			expected:    nil,
		},
		{
			desc: "should return nil when all hosts are empty",
			application: application(
				withTasks(
					task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)),
					task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)),
					task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))),
			),
			expected: nil,
		},
		{
			desc: "with 3 tasks",
			application: application(
				ipAddrPerTask(80),
				withTasks(
					task(ipAddresses("1.1.1.1"), withTaskID("A"), taskPorts(80)),
					task(ipAddresses("1.1.1.2"), withTaskID("B"), taskPorts(80)),
					task(ipAddresses("1.1.1.3"), withTaskID("C"), taskPorts(80))),
			),
			expected: map[string]types.Server{
				"server-A": {
					URL:    "http://1.1.1.1:80",
					Weight: label.DefaultWeight,
				},
				"server-B": {
					URL:    "http://1.1.1.2:80",
					Weight: label.DefaultWeight,
				},
				"server-C": {
					URL:    "http://1.1.1.3:80",
					Weight: label.DefaultWeight,
				},
			},
		},
	}

	p := &Provider{}

	for _, test := range testCases {
		test := test
		if test.desc == "should return nil when all hosts are empty" {
			t.Run(test.desc, func(t *testing.T) {
				t.Parallel()

				actual := p.getServersV1(test.application, test.segmentName)

				assert.Equal(t, test.expected, actual)
			})
		}
	}
}
