// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package conformance

import (
	"context"
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/gruntwork-io/terratest/modules/k8s"
	"github.com/gruntwork-io/terratest/modules/retry"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"sigs.k8s.io/controller-runtime/pkg/client"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

	"github.com/apache/apisix-ingress-controller/internal/types"
	"github.com/apache/apisix-ingress-controller/test/e2e/framework"
	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
)

var gatewayClassName = "apisix"
var controllerName = "apisix.apache.org/apisix-ingress-controller"

var gatewayClass = fmt.Sprintf(`
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: %s
spec:
  controllerName: %s
`, gatewayClassName, controllerName)

var gatewayProxyYaml = `
apiVersion: apisix.apache.org/v1alpha1
kind: GatewayProxy
metadata:
  name: conformance-gateway-proxy
  namespace: %s
spec:
  statusAddress:
    - %s
  provider:
    type: ControlPlane
    controlPlane:
      endpoints:
        - %s
      auth:
        type: AdminKey
        adminKey:
          value: %s
`

type GatewayProxyOpts struct {
	StatusAddress string
	AdminKey      string
	AdminEndpoint string
}

var defaultGatewayProxyOpts GatewayProxyOpts

func deleteNamespace(kubectl *k8s.KubectlOptions) {
	// gateway api conformance test namespaces
	namespacesToDelete := []string{
		"gateway-conformance-infra",
		"gateway-conformance-web-backend",
		"gateway-conformance-app-backend",
		"apisix-conformance-test",
	}

	for _, ns := range namespacesToDelete {
		_, err := k8s.GetNamespaceE(GinkgoT(), kubectl, ns)
		if err == nil {
			// Namespace exists, delete it
			GinkgoT().Logf("Deleting existing namespace: %s", ns)
			err := k8s.DeleteNamespaceE(GinkgoT(), kubectl, ns)
			if err != nil {
				GinkgoT().Logf("Error deleting namespace %s: %v", ns, err)
				continue
			}

			// Wait for deletion to complete by checking until GetNamespaceE returns an error
			_, err = retry.DoWithRetryE(
				GinkgoT(),
				fmt.Sprintf("Waiting for namespace %s to be deleted", ns),
				30,
				5*time.Second,
				func() (string, error) {
					_, err := k8s.GetNamespaceE(GinkgoT(), kubectl, ns)
					if err != nil {
						// Namespace is gone, which is what we want
						return "Namespace deleted", nil
					}
					return "", fmt.Errorf("namespace %s still exists", ns)
				},
			)

			if err != nil {
				GinkgoT().Logf("Error waiting for namespace %s to be deleted: %v", ns, err)
			}
		} else {
			GinkgoT().Logf("Namespace %s does not exist or cannot be accessed", ns)
		}
	}
}

func TestMain(m *testing.M) {
	RegisterFailHandler(Fail)
	f := framework.NewFramework()

	// init newDeployer function
	scaffold.NewDeployer = scaffold.NewAPISIXDeployer

	// Check and delete specific namespaces if they exist
	kubectl := k8s.NewKubectlOptions("", "", "default")
	deleteNamespace(kubectl)

	namespace := "apisix-conformance-test"

	k8s.KubectlApplyFromString(GinkgoT(), kubectl, gatewayClass)
	defer k8s.KubectlDeleteFromString(GinkgoT(), kubectl, gatewayClass)
	k8s.CreateNamespace(GinkgoT(), kubectl, namespace)
	defer k8s.DeleteNamespace(GinkgoT(), kubectl, namespace)

	adminkey := getEnvOrDefault("APISIX_ADMIN_KEY", "edd1c9f034335f136f87ad84b625c8f1")
	controllerName := "apisix.apache.org/apisix-ingress-controller"
	s := scaffold.NewScaffold(scaffold.Options{
		ControllerName:    controllerName,
		SkipHooks:         true,
		APISIXAdminAPIKey: adminkey,
	})

	s.Deployer.DeployDataplane(scaffold.DeployDataplaneOptions{
		AdminKey:          adminkey,
		Namespace:         namespace,
		SkipCreateTunnels: true,
		ServiceType:       "LoadBalancer",
		ServiceHTTPPort:   80,
		ServiceHTTPSPort:  443,
	})
	svc := s.GetDataplaneService()

	if len(svc.Status.LoadBalancer.Ingress) == 0 {
		Fail("No LoadBalancer found for the service")
	}

	address := svc.Status.LoadBalancer.Ingress[0].IP

	f.DeployIngress(framework.IngressDeployOpts{
		ControllerName:     controllerName,
		Namespace:          namespace,
		StatusAddress:      address,
		InitSyncDelay:      20 * time.Minute,
		ProviderType:       framework.ProviderType,
		ProviderSyncPeriod: 1 * time.Hour,
	})

	adminEndpoint := fmt.Sprintf("http://%s.%s:9180", svc.Name, namespace)

	defaultGatewayProxyOpts = GatewayProxyOpts{
		StatusAddress: address,
		AdminKey:      adminkey,
		AdminEndpoint: adminEndpoint,
	}

	patchGatewaysForConformanceTest(context.Background(), f.K8sClient)

	code := m.Run()

	os.Exit(code)
}

func patchGatewaysForConformanceTest(ctx context.Context, k8sClient client.Client) {
	var gatewayProxyMap = make(map[string]bool)

	// list all gateways and patch them
	patchGateway := func(ctx context.Context, k8sClient client.Client) bool {
		gatewayList := &gatewayv1.GatewayList{}
		if err := k8sClient.List(ctx, gatewayList); err != nil {
			return false
		}

		patched := false
		for i := range gatewayList.Items {
			gateway := &gatewayList.Items[i]

			// check if the gateway already has infrastructure.parametersRef
			if gateway.Spec.Infrastructure != nil &&
				gateway.Spec.Infrastructure.ParametersRef != nil {
				continue
			}

			GinkgoT().Logf("Patching Gateway %s", gateway.Name)
			// check if the gateway proxy has been created, if not, create it
			if !gatewayProxyMap[gateway.Namespace] {
				gatewayProxy := fmt.Sprintf(gatewayProxyYaml,
					gateway.Namespace,
					defaultGatewayProxyOpts.StatusAddress,
					defaultGatewayProxyOpts.AdminEndpoint,
					defaultGatewayProxyOpts.AdminKey)
				kubectl := k8s.NewKubectlOptions("", "", gateway.Namespace)
				k8s.KubectlApplyFromString(GinkgoT(), kubectl, gatewayProxy)

				// Mark this namespace as having a GatewayProxy
				gatewayProxyMap[gateway.Namespace] = true
			}

			// add infrastructure.parametersRef
			gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{
				ParametersRef: &gatewayv1.LocalParametersReference{
					Group: "apisix.apache.org",
					Kind:  types.KindGatewayProxy,
					Name:  "conformance-gateway-proxy",
				},
			}

			if err := k8sClient.Update(ctx, gateway); err != nil {
				GinkgoT().Logf("Failed to patch Gateway %s: %v", gateway.Name, err)
				continue
			}

			patched = true
			GinkgoT().Logf("Successfully patched Gateway %s with GatewayProxy reference", gateway.Name)
		}

		return patched
	}

	// continuously monitor and patch gateway resources
	go func() {
		ticker := time.NewTicker(2 * time.Second)
		defer ticker.Stop()

		for {
			select {
			case <-ctx.Done():
				// clean up the gateway proxy
				for namespace := range gatewayProxyMap {
					kubectl := k8s.NewKubectlOptions("", "", namespace)
					_ = k8s.RunKubectlE(GinkgoT(), kubectl, "delete", "gatewayproxy", "conformance-gateway-proxy")
				}
				return
			case <-ticker.C:
				patchGateway(ctx, k8sClient)
			}
		}
	}()
}

// getEnvOrDefault returns environment variable value or default
func getEnvOrDefault(key, defaultValue string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return defaultValue
}
