package route

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	netv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/yaml"
)

type Domains struct {
	Dev     []string `json:"dev" yaml:"dev"`
	Staging []string `json:"staging" yaml:"staging"`
	Prod    []string `json:"prod" yaml:"prod"`
}

type Namespace struct {
	Dev     string `json:"dev" yaml:"dev"`
	Staging string `json:"staging" yaml:"staging"`
	Prod    string `json:"prod" yaml:"prod"`
}

type Service struct {
	ServiceName string            `json:"serviceName" yaml:"serviceName"`
	ServicePort int               `json:"servicePort" yaml:"servicePort"`
	TestCases   []string          `json:"testCases" yaml:"testCases"`
	Regex       []string          `json:"regex" yaml:"regex"`
	Plugins     string            `json:"plugins,omitempty" yaml:"plugins,omitempty"`
	Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}

type Ingress struct {
	Namespace Namespace `json:"namespace" yaml:"namespace"`
	Services  []Service
}

type Config struct {
	Domains           Domains   `json:"domains" yaml:"domains"`
	IngressNamePrefix string    `json:"ingressNamePrefix" yaml:"ingressNamePrefix"`
	Ingresses         []Ingress `json:"ingresses" yaml:"ingresses"`
}

func (c *Config) getDomain() string {
	return c.Domains.Prod[0]
}

func (c *Config) listNamespaces() []string {
	ns := make([]string, 0, 10)
	for _, i := range c.Ingresses {
		ns = append(ns, i.Namespace.Prod)
	}
	return ns
}

func TestCreateIngress(t *testing.T) {
	configFile, err := os.ReadFile("../../deploy/configs/formal.yaml")
	require.NoError(t, err)
	configs := make([]Config, 0, 10)
	err = yaml.Unmarshal(configFile, &configs)
	require.NoError(t, err)

	fmt.Println("INFO: creating namespaces")
	created := map[string]struct{}{}
	for _, item := range configs {
		namespaces := item.listNamespaces()
		for _, namespace := range namespaces {
			if _, ok := created[namespace]; ok {
				continue
			}
			ns := &corev1.Namespace{
				ObjectMeta: metav1.ObjectMeta{Name: namespace},
			}
			_, err := env.Cluster().Client().CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
			require.NoError(t, err)
			created[namespace] = struct{}{}
		}
	}

	fmt.Println("INFO: delay 3 seconds to wait namespace be ready")
	time.Sleep(time.Second * 3)

	fmt.Println("INFO: creating ingresses")
	pathTypePrefix := netv1.PathTypePrefix
	for _, item := range configs {
		for _, i := range item.Ingresses {
			namespace := i.Namespace.Prod
			for _, service := range i.Services {
				ingressName := fmt.Sprintf("%s%s-%d-ingress", item.IngressNamePrefix, service.ServiceName, service.ServicePort)
				paths := make([]netv1.HTTPIngressPath, 0, 10)
				for _, pathRegex := range service.Regex {
					paths = append(paths, netv1.HTTPIngressPath{
						Path:     pathRegex,
						PathType: &pathTypePrefix,
						Backend: netv1.IngressBackend{
							Service: &netv1.IngressServiceBackend{
								Name: service.ServiceName,
								Port: netv1.ServiceBackendPort{
									Number: int32(service.ServicePort),
								},
							},
						},
					})
				}

				// Support annotations in config.yaml
				annotations := map[string]string{
					"kubernetes.io/ingress.class": "kong",
					"konghq.com/strip-path":       "true",
				}
				for k, v := range service.Annotations {
					annotations[k] = v
				}

				fmt.Printf("INFO: ingress %s annotations %v\n", ingressName, annotations)

				ingress := &netv1.Ingress{
					ObjectMeta: metav1.ObjectMeta{
						Name:        ingressName,
						Annotations: annotations,
					},
					Spec: netv1.IngressSpec{
						Rules: []netv1.IngressRule{
							{
								Host: item.getDomain(),
								IngressRuleValue: netv1.IngressRuleValue{
									HTTP: &netv1.HTTPIngressRuleValue{
										Paths: paths,
									},
								},
							},
						},
					},
				}

				_, err := env.Cluster().Client().NetworkingV1().Ingresses(namespace).Create(ctx, ingress, metav1.CreateOptions{})
				require.NoError(t, err)
			}
		}
	}

	fmt.Println("INFO: delay 5 seconds to wait ingresses be ready")
	time.Sleep(time.Second * 5)

	fmt.Println("INFO: run ingress test cases, check whether the urls can be route to right service")
	var wg sync.WaitGroup
	for _, item := range configs {
		for _, i := range item.Ingresses {
			for _, service := range i.Services {
				for _, testCase := range service.TestCases {
					wg.Add(1)
					go func(item Config, i Ingress, service Service, testCase string) {
						defer wg.Done()
						require.Eventually(t, func() bool {
							req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", proxyURL, testCase), nil)
							require.NoError(t, err)
							req.Header.Add("kong-debug", "1")
							req.Header.Add("Host", item.getDomain())
							req.Host = item.getDomain()

							resp, err := httpc.Do(req)
							if err != nil {
								t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err)
								return false
							}
							defer resp.Body.Close()

							serviceName := resp.Header.Get("Kong-Service-Name")
							require.NotEqual(t, "", serviceName, fmt.Sprintf("`%s` can't match any route", testCase))
							routeInfo := strings.Split(serviceName, ".")
							require.Equal(t, i.Namespace.Prod, routeInfo[0], fmt.Sprintf("`%s` namespace unmatch", testCase))
							require.Contains(t, routeInfo[1], service.ServiceName, fmt.Sprintf("`%s` service name unmatch", testCase))
							fmt.Printf("INFO: `%s` success match to %s\n", testCase, serviceName)
							return true
						}, time.Second*5, time.Second)
					}(item, i, service, testCase)
				}
			}
		}
	}
	wg.Wait()
}
