// Copyright Contributors to the KubeOpenCode project

//go:build integration

// This file uses the "integration" build tag to separate envtest-based tests from unit tests.
// This is the standard pattern in the Kubernetes ecosystem (used by kubebuilder, controller-runtime,
// and most operator projects) because it allows tests to remain close to the code they test while
// still enabling separate execution:
//   - `go test ./...` runs only unit tests (no build tag)
//   - `go test -tags=integration ./...` runs integration tests (requires envtest binaries)
//
// Alternative approaches like placing tests in a separate directory (e.g., test/integration/)
// would separate tests from the code they test, making maintenance harder.

// Package controller implements Kubernetes controllers for KubeOpenCode resources
package controller

import (
	"context"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	appsv1 "k8s.io/api/apps/v1"
	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	kubeopenv1alpha1 "github.com/kubeopencode/kubeopencode/api/v1alpha1"
)

var (
	cfg       *rest.Config
	k8sClient client.Client
	testEnv   *envtest.Environment
	ctx       context.Context
	cancel    context.CancelFunc
	scheme    *runtime.Scheme
)

const (
	timeout  = time.Second * 10
	interval = time.Millisecond * 250

	// testAgentName is the name of the shared test Agent created in BeforeSuite.
	// All tests that need an Agent should reference this name in their agentRef.
	testAgentName      = "test-agent"
	testAgentNamespace = "default"
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "deploy", "crds")},
		ErrorIfCRDPathMissing: true,
	}

	var err error
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	scheme = runtime.NewScheme()
	err = kubeopenv1alpha1.AddToScheme(scheme)
	Expect(err).NotTo(HaveOccurred())
	err = corev1.AddToScheme(scheme)
	Expect(err).NotTo(HaveOccurred())
	err = batchv1.AddToScheme(scheme)
	Expect(err).NotTo(HaveOccurred())
	err = appsv1.AddToScheme(scheme)
	Expect(err).NotTo(HaveOccurred())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// Start controllers
	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme,
	})
	Expect(err).ToNot(HaveOccurred())

	err = NewTaskReconciler(
		k8sManager.GetClient(),
		k8sManager.GetScheme(),
		k8sManager.GetEventRecorder("task-controller"),
	).SetupWithManager(k8sManager)
	Expect(err).ToNot(HaveOccurred())

	err = (&AgentReconciler{
		Client:   k8sManager.GetClient(),
		Scheme:   k8sManager.GetScheme(),
		Recorder: k8sManager.GetEventRecorder("agent-controller"),
	}).SetupWithManager(k8sManager)
	Expect(err).ToNot(HaveOccurred())

	err = (&CronTaskReconciler{
		Client:   k8sManager.GetClient(),
		Scheme:   k8sManager.GetScheme(),
		Recorder: k8sManager.GetEventRecorder("crontask-controller"),
	}).SetupWithManager(k8sManager)
	Expect(err).ToNot(HaveOccurred())

	err = (&RegistryReconciler{
		Client: k8sManager.GetClient(),
		Scheme: k8sManager.GetScheme(),
	}).SetupWithManager(k8sManager)
	Expect(err).ToNot(HaveOccurred())

	go func() {
		defer GinkgoRecover()
		err = k8sManager.Start(ctx)
		Expect(err).ToNot(HaveOccurred(), "failed to run manager")
	}()

	// Create shared test Agent for tests that need an Agent
	By("Creating shared test Agent")
	testAgent := &kubeopenv1alpha1.Agent{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testAgentName,
			Namespace: testAgentNamespace,
		},
		Spec: kubeopenv1alpha1.AgentSpec{
			ServiceAccountName: "test-agent-sa",
			WorkspaceDir:       "/workspace",
			Command:            []string{"sh", "-c", "echo 'test agent'"},
		},
	}
	createReadyAgent(ctx, testAgent)
})

var _ = AfterSuite(func() {
	cancel()
	By("tearing down the test environment")
	err := testEnv.Stop()
	Expect(err).NotTo(HaveOccurred())
})

// createReadyAgent creates an Agent and simulates its Deployment being ready.
// In envtest no real pods run, so we must fake Deployment readiness to let the
// Agent controller set status.ready = true.
func createReadyAgent(ctx context.Context, agent *kubeopenv1alpha1.Agent) {
	ExpectWithOffset(1, k8sClient.Create(ctx, agent)).Should(Succeed())

	// Wait for the Agent controller to create the Deployment
	deployName := ServerDeploymentName(agent.Name)
	deployKey := types.NamespacedName{Name: deployName, Namespace: agent.Namespace}
	Eventually(func() error {
		return k8sClient.Get(ctx, deployKey, &appsv1.Deployment{})
	}, timeout, interval).Should(Succeed())

	// Simulate the Deployment having ready replicas (envtest has no kubelet)
	Eventually(func() error {
		deploy := &appsv1.Deployment{}
		if err := k8sClient.Get(ctx, deployKey, deploy); err != nil {
			return err
		}
		deploy.Status.Replicas = 1
		deploy.Status.ReadyReplicas = 1
		deploy.Status.AvailableReplicas = 1
		return k8sClient.Status().Update(ctx, deploy)
	}, timeout, interval).Should(Succeed())

	// Wait for Agent controller to pick up the ready Deployment and set status.ready
	agentKey := types.NamespacedName{Name: agent.Name, Namespace: agent.Namespace}
	Eventually(func() bool {
		a := &kubeopenv1alpha1.Agent{}
		if err := k8sClient.Get(ctx, agentKey, a); err != nil {
			return false
		}
		return a.Status.Ready
	}, timeout, interval).Should(BeTrue())
}
