package operator

import (
	"context"
	"fmt"
	"os/exec"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/cluster"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	v1alpha1 "github.com/pfeifferj/container-mom-go/pkg/apis/containermom/v1alpha1"
)

// mockSuccessCommand creates a command that always returns success
func mockSuccessCommand() *exec.Cmd {
	cmd := exec.Command("echo", "mock success")
	return cmd
}

// TestRouteCreationEndToEnd verifies that routes are created properly
// when a domain is specified in the ContainerMomDeployment
func TestRouteCreationEndToEnd(t *testing.T) {
	// Use a zap logger for easier debugging
	ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

	// Setup a scheme
	scheme := runtime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(scheme))
	require.NoError(t, corev1.AddToScheme(scheme))

	// Setup a mock executor
	mockExecutor := &mockCommandExecutor{
		commands: []string{},
		t:        t,
	}

	// Override exec.Command to use mock
	originalExecCommand := execCommand
	defer func() { execCommand = originalExecCommand }()

	// Replace exec.Command with our mock that simulates namespace and service existence
	execCommand = func(command string, args ...string) *exec.Cmd {
		// Intercept namespace checks to always return success
		if command == "oc" && len(args) >= 2 && (args[0] == "get" && args[1] == "namespace" || args[1] == "ns") {
			// Create a mock command that returns success for namespace checks
			return mockSuccessCommand()
		}

		// Intercept service checks to return success
		if command == "oc" && len(args) >= 2 && args[0] == "get" && args[1] == "service" {
			// Create a mock command that returns success for service checks
			return mockSuccessCommand()
		}

		// For all other commands, use the regular mock
		return mockExecCommand(mockExecutor, command, args...)
	}

	tests := []struct {
		name              string
		domainConfig      v1alpha1.DomainSpec
		expectedHostname  string
		shouldCreateRoute bool
	}{
		{
			name: "with domain name only",
			domainConfig: v1alpha1.DomainSpec{
				DomainName: "example.com",
			},
			expectedHostname:  "example.com",
			shouldCreateRoute: true,
		},
		{
			name: "with prefix and domain",
			domainConfig: v1alpha1.DomainSpec{
				DomainName: "example.com",
				Prefix:     "test",
			},
			expectedHostname:  "test.example.com",
			shouldCreateRoute: true,
		},
		{
			name: "with wildcard domain",
			domainConfig: v1alpha1.DomainSpec{
				DomainName:  "example.com",
				UseWildcard: true,
			},
			expectedHostname:  "*.example.com",
			shouldCreateRoute: true,
		},
		{
			name:              "without domain",
			domainConfig:      v1alpha1.DomainSpec{},
			expectedHostname:  "",
			shouldCreateRoute: false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Reset commands for this test case
			mockExecutor.commands = []string{}

			// Setup test namespace
			namespace := fmt.Sprintf("test-ns-%s", strings.ReplaceAll(tc.name, " ", "-"))
			nsObj := &corev1.Namespace{
				ObjectMeta: metav1.ObjectMeta{
					Name: namespace,
				},
			}

			// Setup deployment with the test domain config
			deployment := &v1alpha1.ContainerMomDeployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-app",
					Namespace: "container-mom-system",
					Annotations: map[string]string{
						"containermom.mom/namespace": namespace,
					},
				},
				Spec: v1alpha1.ContainerMomDeploymentSpec{
					Domain: tc.domainConfig,
					Source: v1alpha1.SourceSpec{
						Image: "nginx:latest",
					},
					Clusters: []string{"local"},
					Resources: v1alpha1.ResourceRequirements{
						CPU:    "100m",
						Memory: "128Mi",
					},
				},
			}

			// Create a fake client with the namespace and deployment
			client := fake.NewClientBuilder().
				WithScheme(scheme).
				WithObjects(nsObj, deployment).
				Build()

			// Create the controller
			controller := &WorkloadController{
				client:            client,
				scheme:            scheme,
				clusters:          make(map[string]cluster.Cluster),
				operatorNamespace: "container-mom-system",
			}

			// Mock the service as if it exists
			if tc.shouldCreateRoute {
				// First try to create a route by directly calling createOrUpdateRoute
				err := controller.createOrUpdateRoute(
					ctrl.LoggerInto(context.Background(), ctrl.Log.WithName("test")),
					deployment,
					namespace,
				)
				assert.NoError(t, err, "Route creation should succeed")

				// Then verify route creation commands were called
				routeCreationCalled := false
				hostnameMatched := false
				for _, cmd := range mockExecutor.commands {
					if strings.Contains(cmd, "create route edge test-app") ||
						strings.Contains(cmd, "patch route test-app") {
						routeCreationCalled = true
						if strings.Contains(cmd, tc.expectedHostname) {
							hostnameMatched = true
						}
					}
				}
				assert.True(t, routeCreationCalled, "Route creation command should be called")
				assert.True(t, hostnameMatched, "Route hostname should match expected value")

				// Clear the commands list
				mockExecutor.commands = []string{}
			}

			// Test the reconcile method
			_, err := controller.Reconcile(
				context.Background(),
				reconcile.Request{NamespacedName: types.NamespacedName{
					Name:      deployment.Name,
					Namespace: deployment.Namespace,
				}},
			)
			assert.NoError(t, err, "Reconcile should succeed")

			// Check if the route creation was called during reconciliation
			if tc.shouldCreateRoute {
				routeCreationCalled := false
				for _, cmd := range mockExecutor.commands {
					if strings.Contains(cmd, "create route") || strings.Contains(cmd, "patch route") {
						routeCreationCalled = true
						break
					}
				}
				assert.True(t, routeCreationCalled, "Route creation should be called during reconcile for domain: %v", tc.domainConfig)
			} else {
				// For empty domain, no route commands should be called
				routeCreationCalled := false
				for _, cmd := range mockExecutor.commands {
					if strings.Contains(cmd, "create route") || strings.Contains(cmd, "patch route") {
						routeCreationCalled = true
						break
					}
				}
				assert.False(t, routeCreationCalled, "No route creation should be called for empty domain")
			}
		})
	}
}

// TestDomainHandlingInReconcileInCluster tests the domain handling in the reconcileInCluster method
func TestDomainHandlingInReconcileInCluster(t *testing.T) {
	// Use a zap logger for easier debugging
	ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

	// Setup a scheme
	scheme := runtime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(scheme))
	require.NoError(t, corev1.AddToScheme(scheme))

	// Setup a mock executor
	mockExecutor := &mockCommandExecutor{
		commands: []string{},
		t:        t,
	}

	// Save original exec.Command and restore it after
	originalExecCommand := execCommand
	defer func() { execCommand = originalExecCommand }()

	// Replace exec.Command with our mock
	execCommand = func(command string, args ...string) *exec.Cmd {
		return mockExecCommand(mockExecutor, command, args...)
	}

	// Create a deployment with domain
	deployment := &v1alpha1.ContainerMomDeployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-app",
			Namespace: "container-mom-system",
			Annotations: map[string]string{
				"containermom.mom/namespace": "test-namespace",
			},
		},
		Spec: v1alpha1.ContainerMomDeploymentSpec{
			Domain: v1alpha1.DomainSpec{
				DomainName: "example.com",
			},
			Source: v1alpha1.SourceSpec{
				Image: "nginx:latest",
			},
			Resources: v1alpha1.ResourceRequirements{
				CPU:    "100m",
				Memory: "128Mi",
			},
		},
	}

	// Create a fake client
	client := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(deployment).
		Build()

	// Create the controller
	controller := &WorkloadController{
		client:            client,
		scheme:            scheme,
		clusters:          make(map[string]cluster.Cluster),
		operatorNamespace: "container-mom-system",
	}

	// Call reconcileInCluster
	err := controller.reconcileInCluster(
		ctrl.LoggerInto(context.Background(), ctrl.Log.WithName("test")),
		client,
		deployment,
	)
	assert.NoError(t, err, "reconcileInCluster should succeed")

	// Check if route creation was called
	routeCreationCalled := false
	for _, cmd := range mockExecutor.commands {
		if strings.Contains(cmd, "create route edge test-app") || strings.Contains(cmd, "patch route test-app") {
			routeCreationCalled = true
			assert.Contains(t, cmd, "example.com", "Route command should include the domain name")
			break
		}
	}
	assert.True(t, routeCreationCalled, "Route creation should be called during reconcileInCluster")
}
