// Copyright (c) 2026 Tigera, Inc. All rights reserved.
//
// 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.

package testutils

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

	v3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
	"github.com/projectcalico/api/pkg/client/clientset_generated/clientset"
	"github.com/projectcalico/api/pkg/client/informers_generated/externalversions"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"

	"github.com/projectcalico/calico/libcalico-go/lib/apiconfig"
	clientv3 "github.com/projectcalico/calico/libcalico-go/lib/clientv3"
	libtestutils "github.com/projectcalico/calico/libcalico-go/lib/testutils"
)

// TestEnv wraps an envtest.Environment with pre-built clients for Calico
// controller FV tests. Use NewTestEnv in TestMain to start the environment,
// and call Stop when the tests are done.
type TestEnv struct {
	Env *envtest.Environment

	// RestConfig is the kube-apiserver connection config.
	RestConfig *rest.Config

	// K8sClient is a standard Kubernetes clientset.
	K8sClient *kubernetes.Clientset

	// CalicoClient is the generated Calico API clientset.
	CalicoClient clientset.Interface

	// Client is a controller-runtime client with the Calico and core K8s schemes registered.
	Client ctrlclient.WithWatch
}

// NewTestEnv creates and starts an envtest environment with all Calico CRDs
// loaded. Additional CRD directory paths can be passed for test-specific CRDs
// (e.g., DatastoreMigration). The caller must call Stop() when done.
func NewTestEnv(extraCRDPaths ...string) (*TestEnv, error) {
	repoRoot := libtestutils.FindRepoRoot()
	crdPaths := []string{
		filepath.Join(repoRoot, "api", "config", "crd"),
		filepath.Join(repoRoot, "libcalico-go", "config", "crd"),
	}
	crdPaths = append(crdPaths, extraCRDPaths...)
	env := &envtest.Environment{
		CRDDirectoryPaths: crdPaths,
	}

	cfg, err := env.Start()
	if err != nil {
		return nil, err
	}
	defer func() {
		if env != nil {
			if err := env.Stop(); err != nil {
				fmt.Fprintf(os.Stderr, "envtest teardown: %v\n", err)
			}
		}
	}()

	// Raise rate limits so test assertions and controllers don't get throttled.
	cfg.QPS = 100
	cfg.Burst = 200

	k8sClient, err := kubernetes.NewForConfig(cfg)
	if err != nil {
		return nil, err
	}

	calicoClient, err := clientset.NewForConfig(cfg)
	if err != nil {
		return nil, err
	}

	scheme := runtime.NewScheme()
	if err := clientgoscheme.AddToScheme(scheme); err != nil {
		return nil, err
	}
	if err := v3.AddToScheme(scheme); err != nil {
		return nil, err
	}
	client, err := ctrlclient.NewWithWatch(cfg, ctrlclient.Options{Scheme: scheme})
	if err != nil {
		return nil, err
	}

	te := &TestEnv{
		Env:          env,
		RestConfig:   cfg,
		K8sClient:    k8sClient,
		CalicoClient: calicoClient,
		Client:       client,
	}
	env = nil // ownership transferred to te; prevent defer from stopping
	return te, nil
}

// NewCalicoInformerFactory creates a shared informer factory for Calico resources
// backed by this environment's API server.
func (te *TestEnv) NewCalicoInformerFactory() externalversions.SharedInformerFactory {
	return externalversions.NewSharedInformerFactory(te.CalicoClient, 0)
}

// NewCalicoEtcdClient creates a libcalico-go client.Interface in EtcdV3 mode
// backed by the same etcd instance that envtest uses for the kube-apiserver.
// Calico stores data under /calico/ while K8s uses /registry/, so there are
// no key conflicts.
func (te *TestEnv) NewCalicoEtcdClient() (clientv3.Interface, error) {
	etcdURL := te.Env.ControlPlane.Etcd.URL
	if etcdURL == nil {
		return nil, fmt.Errorf("envtest etcd URL is nil; was the environment started?")
	}

	cfg := apiconfig.CalicoAPIConfig{
		Spec: apiconfig.CalicoAPIConfigSpec{
			DatastoreType: apiconfig.EtcdV3,
			EtcdConfig: apiconfig.EtcdConfig{
				EtcdEndpoints: etcdURL.String(),
			},
		},
	}
	return clientv3.New(cfg)
}

// Stop tears down the envtest environment.
func (te *TestEnv) Stop() error {
	return te.Env.Stop()
}
