// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

//go:build integration

package k8s_test

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

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"go.uber.org/zap/zapcore"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"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"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

	mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
	"github.com/stacklok/toolhive/pkg/vmcp"
	"github.com/stacklok/toolhive/pkg/vmcp/k8s"
	"github.com/stacklok/toolhive/pkg/vmcp/workloads"
)

// Integration tests for BackendReconciler using envtest
// These tests verify the full reconciliation flow with a real K8s API server

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

func TestBackendReconcilerIntegration(t *testing.T) {
	RegisterFailHandler(Fail)

	suiteConfig, reporterConfig := GinkgoConfiguration()
	reporterConfig.Verbose = false
	reporterConfig.VeryVerbose = false
	reporterConfig.FullTrace = false

	RunSpecs(t, "BackendReconciler Integration Test Suite", suiteConfig, reporterConfig)
}

var _ = BeforeSuite(func() {
	logLevel := zapcore.ErrorLevel
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(logLevel)))

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

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

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

	err = mcpv1beta1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

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

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

var _ = Describe("BackendReconciler Integration Tests", func() {
	const (
		testNamespace = "default"
		testGroupRef  = "test-group"
		timeout       = time.Second * 10
		interval      = time.Millisecond * 250
	)

	var (
		registry         vmcp.DynamicRegistry
		reconcilerMgr    ctrl.Manager
		reconcilerCtx    context.Context
		reconcilerStop   context.CancelFunc
		reconcilerStopped chan struct{}
	)

	BeforeEach(func() {
		// Create a fresh DynamicRegistry for each test
		registry = vmcp.NewDynamicRegistry([]vmcp.Backend{})

		// Create a controller manager for the reconciler
		var err error
		reconcilerMgr, err = ctrl.NewManager(cfg, ctrl.Options{
			Scheme: scheme.Scheme,
			Metrics: metricsserver.Options{
				BindAddress: "0",
			},
			HealthProbeBindAddress: "0",
		})
		Expect(err).NotTo(HaveOccurred())

		// Create discoverer
		discoverer := workloads.NewK8SDiscovererWithClient(k8sClient, testNamespace)

		// Create and register the BackendReconciler
		reconciler := &k8s.BackendReconciler{
			Client:     reconcilerMgr.GetClient(),
			Namespace:  testNamespace,
			GroupRef:   testGroupRef,
			Registry:   registry,
			Discoverer: discoverer,
		}

		err = reconciler.SetupIndexes(ctx, reconcilerMgr)
		Expect(err).NotTo(HaveOccurred())

		err = reconciler.SetupWithManager(reconcilerMgr)
		Expect(err).NotTo(HaveOccurred())

		// Start the manager in a goroutine
		reconcilerCtx, reconcilerStop = context.WithCancel(ctx)
		reconcilerStopped = make(chan struct{})
		go func() {
			defer GinkgoRecover()
			defer close(reconcilerStopped)
			err := reconcilerMgr.Start(reconcilerCtx)
			Expect(err).NotTo(HaveOccurred())
		}()

		// Wait for cache to sync
		Eventually(func() bool {
			return reconcilerMgr.GetCache().WaitForCacheSync(context.Background())
		}, timeout, interval).Should(BeTrue())
	})

	AfterEach(func() {
		// Stop the reconciler manager and wait for it to finish
		if reconcilerStop != nil {
			reconcilerStop()
			// Wait for manager to fully stop with timeout
			select {
			case <-reconcilerStopped:
				// Manager stopped cleanly
			case <-time.After(5 * time.Second):
				Fail("Manager did not stop within timeout")
			}
		}
	})

	Context("MCPServer Lifecycle", func() {
		It("should add MCPServer to registry when created with matching groupRef", func() {
			// Create MCPServer
			mcpServer := &mcpv1beta1.MCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-server-add",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerSpec{
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: testGroupRef},
					Image:     "test-image:latest",
					Transport: "streamable-http",
				},
			}

			Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed())
			defer func() {
				_ = k8sClient.Delete(ctx, mcpServer)
			}()

			// Wait for backend to appear in registry
			// Note: This will fail because GetWorkloadAsVMCPBackend returns nil
			// (no deployment/service exists in envtest), so backend gets removed
			// This is expected behavior - just verifies reconciler runs
			Consistently(func() int {
				return registry.Count()
			}, time.Second*2, interval).Should(Equal(0))
		})

		It("should remove MCPServer from registry when groupRef doesn't match", func() {
			// Create MCPServer with different groupRef
			mcpServer := &mcpv1beta1.MCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-server-mismatch",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerSpec{
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: "different-group"}, // Does NOT match testGroupRef
					Image:     "test-image:latest",
					Transport: "streamable-http",
				},
			}

			Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed())
			defer func() {
				_ = k8sClient.Delete(ctx, mcpServer)
			}()

			// Verify backend is NOT added to registry
			Consistently(func() int {
				return registry.Count()
			}, time.Second*2, interval).Should(Equal(0))
		})

		It("should handle MCPServer deletion (no-op in envtest)", func() {
			// Create MCPServer
			mcpServer := &mcpv1beta1.MCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-server-delete",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerSpec{
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: testGroupRef},
					Image:     "test-image:latest",
					Transport: "streamable-http",
				},
			}

			Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed())

			// Note: Backend is never added because GetWorkloadAsVMCPBackend returns nil
			// (no deployment/service exists in envtest). This test verifies reconciler
			// handles creation/deletion without errors, but can't test actual removal
			// since the backend was never added in the first place.
			Consistently(func() int {
				return registry.Count()
			}, time.Second*2, interval).Should(Equal(0), "Backend should remain not added")

			// Delete the MCPServer
			Expect(k8sClient.Delete(ctx, mcpServer)).Should(Succeed())

			// Verify reconciler handles deletion without errors (still 0 backends)
			Consistently(func() int {
				return registry.Count()
			}, time.Second*2, interval).Should(Equal(0), "Backend count should remain 0")
		})
	})

	Context("MCPRemoteProxy Lifecycle", func() {
		It("should add MCPRemoteProxy to registry when created with matching groupRef", func() {
			// Create MCPRemoteProxy
			proxy := &mcpv1beta1.MCPRemoteProxy{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-proxy-add",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPRemoteProxySpec{
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: testGroupRef},
					RemoteURL: "https://example.com/mcp",
				},
			}

			Expect(k8sClient.Create(ctx, proxy)).Should(Succeed())
			defer func() {
				_ = k8sClient.Delete(ctx, proxy)
			}()

			// Wait for reconciliation
			// Like MCPServer, this will remain at 0 because no actual proxy deployment exists
			Consistently(func() int {
				return registry.Count()
			}, time.Second*2, interval).Should(Equal(0))
		})

		It("should NOT add MCPRemoteProxy with mismatched groupRef", func() {
			// Create MCPRemoteProxy with different groupRef
			proxy := &mcpv1beta1.MCPRemoteProxy{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-proxy-mismatch",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPRemoteProxySpec{
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: "other-group"},
					RemoteURL: "https://example.com/mcp",
				},
			}

			Expect(k8sClient.Create(ctx, proxy)).Should(Succeed())
			defer func() {
				_ = k8sClient.Delete(ctx, proxy)
			}()

			// Verify backend is NOT added
			Consistently(func() int {
				return registry.Count()
			}, time.Second*2, interval).Should(Equal(0))
		})
	})

	Context("Registry Version Tracking", func() {
		It("should increment registry version when resources are created/deleted", func() {
			initialVersion := registry.Version()

			// Create MCPServer
			mcpServer := &mcpv1beta1.MCPServer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-server-version",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerSpec{
					GroupRef: &mcpv1beta1.MCPGroupRef{Name: testGroupRef},
					Image:    "test-image:latest",
					Transport: "streamable-http",
				},
			}

			Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed())

			// Wait for version to change (reconciliation happened)
			Eventually(func() uint64 {
				return registry.Version()
			}, timeout, interval).Should(BeNumerically(">", initialVersion))

			// Delete and verify version increments again
			currentVersion := registry.Version()
			Expect(k8sClient.Delete(ctx, mcpServer)).Should(Succeed())

			Eventually(func() uint64 {
				return registry.Version()
			}, timeout, interval).Should(BeNumerically(">", currentVersion))
		})
	})

	Context("MCPServerEntry Lifecycle", func() {
		It("should add MCPServerEntry to registry when created with matching groupRef", func() {
			entry := &mcpv1beta1.MCPServerEntry{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-entry-add",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerEntrySpec{
					RemoteURL: "https://mcp.example.com/mcp",
					Transport: "streamable-http",
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: testGroupRef},
				},
			}

			Expect(k8sClient.Create(ctx, entry)).Should(Succeed())
			defer func() {
				_ = k8sClient.Delete(ctx, entry)
			}()

			// Set status to Valid so the discoverer accepts it
			entry.Status.Phase = mcpv1beta1.MCPServerEntryPhaseValid
			Expect(k8sClient.Status().Update(ctx, entry)).Should(Succeed())

			// MCPServerEntry uses Spec.RemoteURL directly (no K8s Service needed),
			// so unlike MCPServer/MCPRemoteProxy, the backend should actually be added
			Eventually(func() int {
				return registry.Count()
			}, timeout, interval).Should(Equal(1))

			// Verify the backend has the correct fields
			backend := registry.Get(ctx, "test-entry-add")
			Expect(backend).NotTo(BeNil())
			Expect(backend.BaseURL).To(Equal("https://mcp.example.com/mcp"))
			Expect(backend.TransportType).To(Equal("streamable-http"))
		})

		It("should NOT add MCPServerEntry with mismatched groupRef", func() {
			entry := &mcpv1beta1.MCPServerEntry{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-entry-mismatch",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerEntrySpec{
					RemoteURL: "https://mcp.example.com/mcp",
					Transport: "streamable-http",
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: "other-group"},
				},
			}

			Expect(k8sClient.Create(ctx, entry)).Should(Succeed())
			defer func() {
				_ = k8sClient.Delete(ctx, entry)
			}()

			entry.Status.Phase = mcpv1beta1.MCPServerEntryPhaseValid
			Expect(k8sClient.Status().Update(ctx, entry)).Should(Succeed())

			Consistently(func() int {
				return registry.Count()
			}, time.Second*2, interval).Should(Equal(0))
		})

		It("should remove MCPServerEntry from registry when deleted", func() {
			entry := &mcpv1beta1.MCPServerEntry{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-entry-delete",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerEntrySpec{
					RemoteURL: "https://mcp.example.com/mcp",
					Transport: "streamable-http",
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: testGroupRef},
				},
			}

			Expect(k8sClient.Create(ctx, entry)).Should(Succeed())

			entry.Status.Phase = mcpv1beta1.MCPServerEntryPhaseValid
			Expect(k8sClient.Status().Update(ctx, entry)).Should(Succeed())

			// Wait for backend to appear
			Eventually(func() int {
				return registry.Count()
			}, timeout, interval).Should(Equal(1))

			// Delete the entry
			Expect(k8sClient.Delete(ctx, entry)).Should(Succeed())

			// Wait for backend to be removed
			Eventually(func() int {
				return registry.Count()
			}, timeout, interval).Should(Equal(0))
		})

		It("should increment registry version on MCPServerEntry events", func() {
			initialVersion := registry.Version()

			entry := &mcpv1beta1.MCPServerEntry{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-entry-version",
					Namespace: testNamespace,
				},
				Spec: mcpv1beta1.MCPServerEntrySpec{
					RemoteURL: "https://mcp.example.com/mcp",
					Transport: "streamable-http",
					GroupRef:  &mcpv1beta1.MCPGroupRef{Name: testGroupRef},
				},
			}

			Expect(k8sClient.Create(ctx, entry)).Should(Succeed())

			entry.Status.Phase = mcpv1beta1.MCPServerEntryPhaseValid
			Expect(k8sClient.Status().Update(ctx, entry)).Should(Succeed())

			Eventually(func() uint64 {
				return registry.Version()
			}, timeout, interval).Should(BeNumerically(">", initialVersion))

			currentVersion := registry.Version()
			Expect(k8sClient.Delete(ctx, entry)).Should(Succeed())

			Eventually(func() uint64 {
				return registry.Version()
			}, timeout, interval).Should(BeNumerically(">", currentVersion))
		})
	})
})
