// Copyright 2026 PingCAP, Inc.
//
// Licensed 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.

//go:build e2e

// Package framework provides lifecycle helpers for tests that run against a
// real Kubernetes API server.
package framework

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/onsi/ginkgo/v2"
	"github.com/onsi/gomega"
	tioperatorv1alpha1 "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
	metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/api/v1alpha1"
	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/test/e2e/waiter"
)

const (
	apiTimeout              = 30 * time.Second
	namespaceCleanupTimeout = 5 * time.Minute
	pollInterval            = 200 * time.Millisecond
)

// Framework owns the clients and temporary test resources for one e2e suite.
type Framework struct {
	Client  client.Client
	Dynamic dynamic.Interface
	Metrics metricsclient.Interface
	config  config
}

type config struct {
	adminNamespace            string
	capacityPlannerNamespace  string
	capacityPlannerDeployment string
	tiDBOperatorRelease       string
	sqlWorkloadImage          string
}

// New parses all e2e configuration and creates uncached clients. Specs do not
// read environment variables directly and never fall back to an ambient
// kubeconfig.
func New() *Framework {
	envConfig := config{
		adminNamespace:            requiredEnv("ADMIN_NAMESPACE"),
		capacityPlannerNamespace:  requiredEnv("CAPACITY_PLANNER_NAMESPACE"),
		capacityPlannerDeployment: requiredEnv("CAPACITY_PLANNER_DEPLOYMENT"),
		tiDBOperatorRelease:       requiredEnv("TIDB_OPERATOR_RELEASE"),
		sqlWorkloadImage:          requiredEnv("SQL_WORKLOAD_IMAGE"),
	}
	kubeconfig := requiredPathEnv("KUBECONFIG")
	cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	must(err, "load kubeconfig %q", kubeconfig)
	cfg.UserAgent = "capacity-planner-e2e"
	cfg.Timeout = apiTimeout

	scheme := runtime.NewScheme()
	must(appsv1.AddToScheme(scheme), "add apps API to scheme")
	must(corev1.AddToScheme(scheme), "add core API to scheme")
	must(v1alpha1.AddToScheme(scheme), "add Autoscaler API to scheme")
	must(tioperatorv1alpha1.AddToScheme(scheme), "add TiDB Operator API to scheme")

	c, err := client.New(cfg, client.Options{Scheme: scheme})
	must(err, "create Kubernetes client")
	dynamicClient, err := dynamic.NewForConfig(cfg)
	must(err, "create dynamic Kubernetes client")
	metricsClient, err := metricsclient.NewForConfig(cfg)
	must(err, "create Kubernetes metrics client")

	return &Framework{
		Client:  c,
		Dynamic: dynamicClient,
		Metrics: metricsClient,
		config:  envConfig,
	}
}

func requiredEnv(name string) string {
	value := strings.TrimSpace(os.Getenv(name))
	gomega.ExpectWithOffset(1, value).NotTo(gomega.BeEmpty(), "%s must be set", name)
	return value
}

func requiredPathEnv(name string) string {
	value := requiredEnv(name)
	abs, err := filepath.Abs(value)
	must(err, "resolve %s=%q", name, value)
	info, err := os.Stat(abs)
	must(err, "stat %s=%q", name, abs)
	gomega.ExpectWithOffset(1, info.Mode().IsRegular()).To(gomega.BeTrue(), "%s=%q is not a regular file", name, abs)
	return abs
}

// SQLWorkloadImage returns the image selected when New parsed the e2e
// environment.
func (f *Framework) SQLWorkloadImage() string {
	return f.config.sqlWorkloadImage
}

// CreateNamespace creates the suite's unique namespace. When called from an
// Ordered BeforeAll, DeferCleanup removes it after every spec has completed.
func (f *Framework) CreateNamespace(ctx context.Context) string {
	ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: "e2e-capacity-planner-"}}
	opCtx, cancel := context.WithTimeout(ctx, apiTimeout)
	defer cancel()
	must(f.Client.Create(opCtx, ns), "create e2e namespace")
	fmt.Fprintf(ginkgo.GinkgoWriter, "created namespace %s\n", ns.Name)

	ginkgo.DeferCleanup(func(ctx context.Context) {
		must(f.deleteNamespace(ctx, ns.Name), "delete e2e namespace %q", ns.Name)
	}, ginkgo.NodeTimeout(namespaceCleanupTimeout))
	return ns.Name
}

func (f *Framework) deleteNamespace(ctx context.Context, namespace string) error {
	cleanupCtx, cancel := context.WithTimeout(ctx, namespaceCleanupTimeout)
	defer cancel()
	err := f.Client.Delete(cleanupCtx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})
	if err != nil && !apierrors.IsNotFound(err) {
		return fmt.Errorf("delete namespace %q: %w", namespace, err)
	}
	if err := waiter.NamespaceDeleted(cleanupCtx, f.Client, namespace); err != nil {
		return fmt.Errorf("wait for namespace %q deletion: %w", namespace, err)
	}
	return nil
}

func must(err error, format string, args ...any) {
	gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred(), fmt.Sprintf(format, args...))
}
