package kubernetes

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"testing"

	"github.com/docker/cli/cli/compose/loader"
	composetypes "github.com/docker/cli/cli/compose/types"
	"github.com/docker/compose-on-kubernetes/api/compose/v1alpha3"
	"github.com/docker/compose-on-kubernetes/api/compose/v1beta1"
	"github.com/docker/compose-on-kubernetes/api/compose/v1beta2"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestNewStackConverter(t *testing.T) {
	_, err := NewStackConverter("v1alpha1")
	assert.Check(t, is.ErrorContains(err, "stack version v1alpha1 unsupported"))

	_, err = NewStackConverter("v1beta1")
	assert.NilError(t, err)
	_, err = NewStackConverter("v1beta2")
	assert.NilError(t, err)
	_, err = NewStackConverter("v1alpha3")
	assert.NilError(t, err)
}

func TestConvertFromToV1beta1(t *testing.T) {
	composefile := `version: "3.3"
services: 
  test:
    image: nginx
secrets:
  test:
    file: testdata/secret
configs:
  test:
    file: testdata/config
`
	stackv1beta1 := &v1beta1.Stack{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test",
		},
		Spec: v1beta1.StackSpec{
			ComposeFile: composefile,
		},
	}

	result, err := stackFromV1beta1(stackv1beta1)
	assert.NilError(t, err)
	expected := Stack{
		Name:        "test",
		ComposeFile: composefile,
		Spec: &v1alpha3.StackSpec{
			Services: []v1alpha3.ServiceConfig{
				{
					Name:        "test",
					Image:       "nginx",
					Environment: make(map[string]*string),
				},
			},
			Secrets: map[string]v1alpha3.SecretConfig{
				"test": {File: filepath.FromSlash("testdata/secret")},
			},
			Configs: map[string]v1alpha3.ConfigObjConfig{
				"test": {File: filepath.FromSlash("testdata/config")},
			},
		},
	}
	assert.DeepEqual(t, expected, result)
	assert.DeepEqual(t, stackv1beta1, stackToV1beta1(result))
}

func TestConvertFromToV1beta2(t *testing.T) {
	stackv1beta2 := &v1beta2.Stack{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test",
		},
		Spec: &v1beta2.StackSpec{
			Services: []v1beta2.ServiceConfig{
				{
					Name:        "test",
					Image:       "nginx",
					Environment: make(map[string]*string),
				},
			},
			Secrets: map[string]v1beta2.SecretConfig{
				"test": {File: filepath.FromSlash("testdata/secret")},
			},
			Configs: map[string]v1beta2.ConfigObjConfig{
				"test": {File: filepath.FromSlash("testdata/config")},
			},
		},
	}
	expected := Stack{
		Name: "test",
		Spec: &v1alpha3.StackSpec{
			Services: []v1alpha3.ServiceConfig{
				{
					Name:        "test",
					Image:       "nginx",
					Environment: make(map[string]*string),
				},
			},
			Secrets: map[string]v1alpha3.SecretConfig{
				"test": {File: filepath.FromSlash("testdata/secret")},
			},
			Configs: map[string]v1alpha3.ConfigObjConfig{
				"test": {File: filepath.FromSlash("testdata/config")},
			},
		},
	}
	result, err := stackFromV1beta2(stackv1beta2)
	assert.NilError(t, err)
	assert.DeepEqual(t, expected, result)
	gotBack, err := stackToV1beta2(result)
	assert.NilError(t, err)
	assert.DeepEqual(t, stackv1beta2, gotBack)
}

func TestConvertFromToV1alpha3(t *testing.T) {
	stackv1alpha3 := &v1alpha3.Stack{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test",
		},
		Spec: &v1alpha3.StackSpec{
			Services: []v1alpha3.ServiceConfig{
				{
					Name:        "test",
					Image:       "nginx",
					Environment: make(map[string]*string),
				},
			},
			Secrets: map[string]v1alpha3.SecretConfig{
				"test": {File: filepath.FromSlash("testdata/secret")},
			},
			Configs: map[string]v1alpha3.ConfigObjConfig{
				"test": {File: filepath.FromSlash("testdata/config")},
			},
		},
	}
	expected := Stack{
		Name: "test",
		Spec: &v1alpha3.StackSpec{
			Services: []v1alpha3.ServiceConfig{
				{
					Name:        "test",
					Image:       "nginx",
					Environment: make(map[string]*string),
				},
			},
			Secrets: map[string]v1alpha3.SecretConfig{
				"test": {File: filepath.FromSlash("testdata/secret")},
			},
			Configs: map[string]v1alpha3.ConfigObjConfig{
				"test": {File: filepath.FromSlash("testdata/config")},
			},
		},
	}
	result := stackFromV1alpha3(stackv1alpha3)
	assert.DeepEqual(t, expected, result)
	gotBack := stackToV1alpha3(result)
	assert.DeepEqual(t, stackv1alpha3, gotBack)
}

func loadTestStackWith(t *testing.T, with string) *composetypes.Config {
	t.Helper()
	filePath := fmt.Sprintf("testdata/compose-with-%s.yml", with)
	data, err := os.ReadFile(filePath)
	assert.NilError(t, err)
	yamlData, err := loader.ParseYAML(data)
	assert.NilError(t, err)
	cfg, err := loader.Load(composetypes.ConfigDetails{
		ConfigFiles: []composetypes.ConfigFile{
			{Config: yamlData, Filename: filePath},
		},
	})
	assert.NilError(t, err)
	return cfg
}

func TestHandlePullSecret(t *testing.T) {
	testData := loadTestStackWith(t, "pull-secret")
	cases := []struct {
		version string
		err     string
	}{
		{version: "v1beta1", err: `stack API version v1beta1 does not support pull secrets (field "x-kubernetes.pull_secret"), please use version v1alpha3 or higher`},
		{version: "v1beta2", err: `stack API version v1beta2 does not support pull secrets (field "x-kubernetes.pull_secret"), please use version v1alpha3 or higher`},
		{version: "v1alpha3"},
	}

	for _, c := range cases {
		c := c
		t.Run(c.version, func(t *testing.T) {
			conv, err := NewStackConverter(c.version)
			assert.NilError(t, err)
			s, err := conv.FromCompose(io.Discard, "test", testData)
			if c.err != "" {
				assert.Error(t, err, c.err)

			} else {
				assert.NilError(t, err)
				assert.Equal(t, s.Spec.Services[0].PullSecret, "some-secret")
			}
		})
	}
}

func TestHandlePullPolicy(t *testing.T) {
	testData := loadTestStackWith(t, "pull-policy")
	cases := []struct {
		version string
		err     string
	}{
		{version: "v1beta1", err: `stack API version v1beta1 does not support pull policies (field "x-kubernetes.pull_policy"), please use version v1alpha3 or higher`},
		{version: "v1beta2", err: `stack API version v1beta2 does not support pull policies (field "x-kubernetes.pull_policy"), please use version v1alpha3 or higher`},
		{version: "v1alpha3"},
	}

	for _, c := range cases {
		c := c
		t.Run(c.version, func(t *testing.T) {
			conv, err := NewStackConverter(c.version)
			assert.NilError(t, err)
			s, err := conv.FromCompose(io.Discard, "test", testData)
			if c.err != "" {
				assert.Error(t, err, c.err)

			} else {
				assert.NilError(t, err)
				assert.Equal(t, s.Spec.Services[0].PullPolicy, "Never")
			}
		})
	}
}

func TestHandleInternalServiceType(t *testing.T) {
	cases := []struct {
		name     string
		value    string
		caps     composeCapabilities
		err      string
		expected v1alpha3.InternalServiceType
	}{
		{
			name:  "v1beta1",
			value: "ClusterIP",
			caps:  v1beta1Capabilities,
			err:   `stack API version v1beta1 does not support intra-stack load balancing (field "x-kubernetes.internal_service_type"), please use version v1alpha3 or higher`,
		},
		{
			name:  "v1beta2",
			value: "ClusterIP",
			caps:  v1beta2Capabilities,
			err:   `stack API version v1beta2 does not support intra-stack load balancing (field "x-kubernetes.internal_service_type"), please use version v1alpha3 or higher`,
		},
		{
			name:     "v1alpha3",
			value:    "ClusterIP",
			caps:     v1alpha3Capabilities,
			expected: v1alpha3.InternalServiceTypeClusterIP,
		},
		{
			name:  "v1alpha3-invalid",
			value: "invalid",
			caps:  v1alpha3Capabilities,
			err:   `invalid value "invalid" for field "x-kubernetes.internal_service_type", valid values are "ClusterIP" or "Headless"`,
		},
	}
	for _, c := range cases {
		c := c
		t.Run(c.name, func(t *testing.T) {
			res, err := fromComposeServiceConfig(composetypes.ServiceConfig{
				Name:  "test",
				Image: "test",
				Extras: map[string]interface{}{
					"x-kubernetes": map[string]interface{}{
						"internal_service_type": c.value,
					},
				},
			}, c.caps)
			if c.err == "" {
				assert.NilError(t, err)
				assert.Equal(t, res.InternalServiceType, c.expected)
			} else {
				assert.ErrorContains(t, err, c.err)
			}
		})
	}
}

func TestIgnoreExpose(t *testing.T) {
	testData := loadTestStackWith(t, "expose")
	for _, version := range []string{"v1beta1", "v1beta2"} {
		conv, err := NewStackConverter(version)
		assert.NilError(t, err)
		s, err := conv.FromCompose(io.Discard, "test", testData)
		assert.NilError(t, err)
		assert.Equal(t, len(s.Spec.Services[0].InternalPorts), 0)
	}
}

func TestParseExpose(t *testing.T) {
	testData := loadTestStackWith(t, "expose")
	conv, err := NewStackConverter("v1alpha3")
	assert.NilError(t, err)
	s, err := conv.FromCompose(io.Discard, "test", testData)
	assert.NilError(t, err)
	expected := []v1alpha3.InternalPort{
		{
			Port:     1,
			Protocol: v1.ProtocolTCP,
		},
		{
			Port:     2,
			Protocol: v1.ProtocolTCP,
		},
		{
			Port:     3,
			Protocol: v1.ProtocolTCP,
		},
		{
			Port:     4,
			Protocol: v1.ProtocolTCP,
		},
		{
			Port:     5,
			Protocol: v1.ProtocolUDP,
		},
		{
			Port:     6,
			Protocol: v1.ProtocolUDP,
		},
		{
			Port:     7,
			Protocol: v1.ProtocolUDP,
		},
		{
			Port:     8,
			Protocol: v1.ProtocolUDP,
		},
	}
	assert.DeepEqual(t, s.Spec.Services[0].InternalPorts, expected)
}
