package operator

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/go-logr/zapr"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	v1alpha1 "github.com/pfeifferj/container-mom-go/pkg/apis/containermom/v1alpha1"
	"github.com/pfeifferj/container-mom-go/pkg/billing"
	"github.com/pfeifferj/container-mom-go/pkg/config"
	testutil "github.com/pfeifferj/container-mom-go/pkg/operator/testing"
)

const (
	// Set reasonable timeouts for all tests
	testTimeout = 5 * time.Second
)

// setupTestLogger configures a development logger for testing
func setupTestLogger() {
	zapCfg := zap.NewDevelopmentConfig()
	// Reduce memory usage by limiting the size of stacks
	zapCfg.DisableStacktrace = true
	zapLog, err := zapCfg.Build()
	if err != nil {
		panic(fmt.Sprintf("failed to create zap logger: %v", err))
	}
	logger := zapr.NewLogger(zapLog)
	ctrl.SetLogger(logger)
}

// setupTimeoutContext creates a context with a timeout for tests
func setupTimeoutContext(t *testing.T) (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), testTimeout)
}

// TestOperatorInitialization tests the creation of a new Operator
func TestOperatorInitialization(t *testing.T) {
	// Set up logging
	setupTestLogger()

	// Create test config
	cfg := &config.Config{
		ListenAddr: ":8081",
	}

	// Create a mock billing service
	billingService := &billing.Service{}

	// Create operator
	op, err := New(cfg, billingService)

	// Verify operator creation
	assert.NoError(t, err, "Operator creation should not error")
	assert.NotNil(t, op, "Operator should not be nil")
	assert.Equal(t, cfg, op.config, "Operator should have the provided config")
	assert.Equal(t, billingService, op.billing, "Operator should have the provided billing service")
	assert.NotNil(t, op.client, "Kubernetes client should be initialized")
}

// MockDeploymentController is a mock deployment controller for testing
type MockDeploymentController struct{}

// Start is a no-op for testing
func (m *MockDeploymentController) Start(context.Context) error {
	return nil
}

// reconcileDeployment is a mock implementation that always succeeds
func (m *MockDeploymentController) reconcileDeployment(ctx context.Context, deploymentID string) error {
	return nil
}

// EnqueueDeployment is a no-op for testing
func (m *MockDeploymentController) EnqueueDeployment(deploymentID string) {}

// TestOperatorDeploymentTrigger tests the TriggerDeploymentReconcile functionality
func TestOperatorDeploymentTrigger(t *testing.T) {
	// Set up logging
	setupTestLogger()

	// Set up timeout context
	ctx, cancel := setupTimeoutContext(t)
	defer cancel()

	// Create test config
	cfg := &config.Config{
		ListenAddr: ":8081",
	}

	// Create a fake k8s client
	k8sClient, err := testutil.NewFakeClient()
	require.NoError(t, err, "Failed to create fake client")

	// Create an operator with uninitialized controller
	op := &Operator{
		config: cfg,
		client: k8sClient,
		// controller is intentionally nil for this test
	}
	assert.NotNil(t, op)

	// Test deployment trigger with uninitialized operator
	// Should fail with a specific error about controller not being initialized
	err = op.TriggerDeploymentReconcile(ctx, "test-deployment")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "deployment controller not initialized")

	// Create a test deployment - simplified to reduce memory usage
	testDeployment := &v1alpha1.ContainerMomDeployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-deployment",
			Namespace: "container-mom-system",
		},
		Spec: v1alpha1.ContainerMomDeploymentSpec{
			Source: v1alpha1.SourceSpec{
				GitURL: "https://github.com/test/repo",
			},
		},
	}

	err = k8sClient.Create(ctx, testDeployment)
	require.NoError(t, err, "Failed to create test deployment")

	// Cleanup the deployment when done to avoid memory leaks
	defer func() {
		err := k8sClient.Delete(ctx, testDeployment)
		if err != nil && !errors.IsNotFound(err) {
			t.Logf("Warning: Failed to clean up test deployment: %v", err)
		}
	}()

	// Initialize with a mock controller instead of a real one
	op.controller = &MockDeploymentController{}

	// Should succeed now with controller initialized
	err = op.TriggerDeploymentReconcile(ctx, "test-deployment")
	assert.NoError(t, err, "TriggerDeploymentReconcile should not error with initialized controller")
}

// TestOperatorBillingFunctions tests all billing-related functions
func TestOperatorBillingFunctions(t *testing.T) {
	// Set up timeout context
	ctx, cancel := setupTimeoutContext(t)
	defer cancel()

	op := &Operator{}

	t.Run("GetCurrentCustomer", func(t *testing.T) {
		customer, err := op.GetCurrentCustomer(ctx)
		assert.NoError(t, err)
		assert.Equal(t, "default", customer.Name)
	})

	t.Run("GetResourceUsage", func(t *testing.T) {
		usage, err := op.GetResourceUsage(ctx, "test-customer")
		assert.NoError(t, err)
		assert.NotNil(t, usage)
	})

	t.Run("GetBillingHistory", func(t *testing.T) {
		history, err := op.GetBillingHistory(ctx, "test-customer")
		assert.NoError(t, err)
		assert.Empty(t, history)
	})

	t.Run("UpdateBillingPlan", func(t *testing.T) {
		err := op.UpdateBillingPlan(ctx, "test-customer", "premium-plan")
		assert.NoError(t, err)
	})

	t.Run("UpdateBillingStatus", func(t *testing.T) {
		status := v1alpha1.BillingStatus{
			Status:  "active",
			Message: "test status",
		}
		err := op.UpdateBillingStatus(ctx, "test-customer", status)
		assert.NoError(t, err)
	})
}

// TestSchemeRegistration tests that all required types are registered in the operator's scheme
func TestSchemeRegistration(t *testing.T) {
	// Test ContainerMomDeployment registration
	gvk := v1alpha1.SchemeGroupVersion.WithKind("ContainerMomDeployment")
	obj, err := scheme.New(gvk)
	assert.NoError(t, err, "ContainerMomDeployment should be registered in scheme")
	assert.NotNil(t, obj)
	assert.IsType(t, &v1alpha1.ContainerMomDeployment{}, obj)

	// Test ContainerMomDeploymentList registration
	listGVK := v1alpha1.SchemeGroupVersion.WithKind("ContainerMomDeploymentList")
	listObj, err := scheme.New(listGVK)
	assert.NoError(t, err, "ContainerMomDeploymentList should be registered in scheme")
	assert.NotNil(t, listObj)
	assert.IsType(t, &v1alpha1.ContainerMomDeploymentList{}, listObj)

	// Test Template registration
	templateGVK := v1alpha1.SchemeGroupVersion.WithKind("Template")
	templateObj, err := scheme.New(templateGVK)
	assert.NoError(t, err, "Template should be registered in scheme")
	assert.NotNil(t, templateObj)
	assert.IsType(t, &v1alpha1.Template{}, templateObj)

	// Test TemplateList registration
	templateListGVK := v1alpha1.SchemeGroupVersion.WithKind("TemplateList")
	templateListObj, err := scheme.New(templateListGVK)
	assert.NoError(t, err, "TemplateList should be registered in scheme")
	assert.NotNil(t, templateListObj)
	assert.IsType(t, &v1alpha1.TemplateList{}, templateListObj)
}

// TestOperatorRun tests basic startup of the Operator.Run function
func TestOperatorRun(t *testing.T) {
	// Skip for normal test runs as this requires network resources
	t.Skip("Skipping TestOperatorRun as it requires network resources")
}

// MockTemplateReconciler is a mock implementation for tests
type MockTemplateReconciler struct {
	Client client.Client
}

// InstantiateTemplate implements the TemplateController interface for testing
func (r *MockTemplateReconciler) InstantiateTemplate(ctx context.Context, templateName string, namespace string, params map[string]string) error {
	// For testing, we'll just retrieve the template to verify it exists
	template := &v1alpha1.Template{}
	err := r.Client.Get(ctx, types.NamespacedName{Name: templateName, Namespace: namespace}, template)
	return err
}

// TestInstantiateTemplate tests the template instantiation functionality
func TestInstantiateTemplate(t *testing.T) {
	// Set up logging with reduced memory footprint
	setupTestLogger()

	// Set up timeout context
	ctx, cancel := setupTimeoutContext(t)
	defer cancel()

	// Create fake k8s client
	k8sClient, err := testutil.NewFakeClient()
	require.NoError(t, err, "Failed to create fake client")

	// Create a simplified template to reduce memory usage
	template := &v1alpha1.Template{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-template",
			Namespace: "default",
		},
		Spec: v1alpha1.TemplateSpec{
			Components: []v1alpha1.TemplateComponent{
				{
					Type: "deployment",
					Name: "app",
					Properties: v1alpha1.TemplateComponentProperties{
						Image: "nginx:latest",
						Env: []v1alpha1.EnvVar{
							{
								Name:  "TEST_VAR",
								Value: "${TEST_PARAM}",
							},
						},
					},
				},
			},
		},
	}

	// Create the template
	err = k8sClient.Create(ctx, template)
	require.NoError(t, err, "Failed to create test template")

	// Cleanup after the test
	defer func() {
		err := k8sClient.Delete(ctx, template)
		if err != nil && !errors.IsNotFound(err) {
			t.Logf("Warning: Failed to clean up test template: %v", err)
		}
	}()

	// Create parameters for template instantiation
	params := map[string]string{
		"TEST_PARAM": "test-value",
	}

	// Create a test operator with a mock template controller
	op := &Operator{
		client: k8sClient,
		templateController: &MockTemplateReconciler{
			Client: k8sClient,
		},
	}

	// Test instantiating the template
	err = op.InstantiateTemplate(ctx, "test-template", "default", params)
	assert.NoError(t, err, "InstantiateTemplate should not error")
}

// TestSubstituteParams tests the parameter substitution functions
func TestSubstituteParams(t *testing.T) {
	// Original environment variables with placeholders
	envVars := []v1alpha1.EnvVar{
		{
			Name:  "NAME",
			Value: "${name}",
		},
		{
			Name:  "FULL_GREETING",
			Value: "Hello, ${name}!",
		},
		{
			Name:  "NO_SUBST",
			Value: "Static Value",
		},
	}

	// Parameters for substitution
	params := map[string]string{
		"name": "Test User",
	}

	// Substitute parameters in environment variables
	result := substituteParams(envVars, params)

	// Verify substitution
	assert.Equal(t, "Test User", result[0].Value, "Simple substitution failed")
	assert.Equal(t, "Hello, Test User!", result[1].Value, "Partial substitution failed")
	assert.Equal(t, "Static Value", result[2].Value, "Static value should remain unchanged")
}

// TestFindPrimaryComponent tests the primary component finder
func TestFindPrimaryComponent(t *testing.T) {
	// Case 1: Template with a specific component that should be identified as primary
	templateWithSpecificComponent := &v1alpha1.Template{
		Spec: v1alpha1.TemplateSpec{
			Components: []v1alpha1.TemplateComponent{
				{
					Type: "service",
					Name: "component-1",
				},
				{
					Type: "deployment",
					Name: "component-2",
				},
				{
					Type: "configmap",
					Name: "component-3",
				},
			},
		},
	}

	primary := findPrimaryComponent(templateWithSpecificComponent)
	assert.NotNil(t, primary, "Should find primary component")
	// The actual implementation of findPrimaryComponent would determine which is primary
	// based on its own logic, not a "Primary" field

	// Case 2: Template with only one component should return that one
	templateWithOneComponent := &v1alpha1.Template{
		Spec: v1alpha1.TemplateSpec{
			Components: []v1alpha1.TemplateComponent{
				{
					Type: "deployment",
					Name: "first-component",
				},
			},
		},
	}

	primary = findPrimaryComponent(templateWithOneComponent)
	assert.NotNil(t, primary, "Should find a default primary component")
	assert.Equal(t, "first-component", primary.Name, "Should use the only component as primary")

	// Case 3: Empty template should return nil
	emptyTemplate := &v1alpha1.Template{
		Spec: v1alpha1.TemplateSpec{
			Components: []v1alpha1.TemplateComponent{},
		},
	}

	primary = findPrimaryComponent(emptyTemplate)
	assert.Nil(t, primary, "Should return nil for empty template")
}
