package deployer

import (
	"context"
	"testing"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes/fake"
)

func newTestDeployer(client *fake.Clientset) *Deployer {
	return NewDeployer(client, Config{Namespace: "projects"})
}

func createFakeDeployment(t *testing.T, client *fake.Clientset, name, project string) {
	t.Helper()
	replicas := int32(1)
	_, err := client.AppsV1().Deployments("projects").Create(context.Background(), &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: "projects",
			Labels:    map[string]string{"app": name, "project": project},
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": name}},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": name}},
				Spec:       corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "test:latest"}}},
			},
		},
	}, metav1.CreateOptions{})
	if err != nil {
		t.Fatalf("failed to create deployment %s: %v", name, err)
	}
}

func createFakeService(t *testing.T, client *fake.Clientset, name, project string) {
	t.Helper()
	_, err := client.CoreV1().Services("projects").Create(context.Background(), &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: "projects",
			Labels:    map[string]string{"app": name, "project": project},
		},
		Spec: corev1.ServiceSpec{
			Selector: map[string]string{"app": name},
			Ports:    []corev1.ServicePort{{Port: 8080}},
		},
	}, metav1.CreateOptions{})
	if err != nil {
		t.Fatalf("failed to create service %s: %v", name, err)
	}
}

func createFakeIngress(t *testing.T, client *fake.Clientset, name, project string) {
	t.Helper()
	_, err := client.NetworkingV1().Ingresses("projects").Create(context.Background(), &networkingv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: "projects",
			Labels:    map[string]string{"app": name, "project": project},
		},
	}, metav1.CreateOptions{})
	if err != nil {
		t.Fatalf("failed to create ingress %s: %v", name, err)
	}
}

func createFakeSecret(t *testing.T, client *fake.Clientset, name, project string) {
	t.Helper()
	_, err := client.CoreV1().Secrets("projects").Create(context.Background(), &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: "projects",
			Labels:    map[string]string{"project": project},
		},
	}, metav1.CreateOptions{})
	if err != nil {
		t.Fatalf("failed to create secret %s: %v", name, err)
	}
}

func TestUndeployAll_SingleApp(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployer(client)
	ctx := context.Background()

	createFakeDeployment(t, client, "myapp", "myapp")
	createFakeService(t, client, "myapp", "myapp")
	createFakeIngress(t, client, "myapp", "myapp")
	createFakeSecret(t, client, "myapp-env", "myapp")

	if err := d.UndeployAll(ctx, "myapp"); err != nil {
		t.Fatalf("UndeployAll failed: %v", err)
	}

	deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
	if len(deps.Items) != 0 {
		t.Errorf("expected 0 deployments, got %d", len(deps.Items))
	}
	svcs, _ := client.CoreV1().Services("projects").List(ctx, metav1.ListOptions{})
	if len(svcs.Items) != 0 {
		t.Errorf("expected 0 services, got %d", len(svcs.Items))
	}
	ings, _ := client.NetworkingV1().Ingresses("projects").List(ctx, metav1.ListOptions{})
	if len(ings.Items) != 0 {
		t.Errorf("expected 0 ingresses, got %d", len(ings.Items))
	}
	secs, _ := client.CoreV1().Secrets("projects").List(ctx, metav1.ListOptions{})
	if len(secs.Items) != 0 {
		t.Errorf("expected 0 secrets, got %d", len(secs.Items))
	}
}

func TestUndeployAll_MonorepoComponents(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployer(client)
	ctx := context.Background()

	// Main project + two components all sharing "project=myapp" label
	createFakeDeployment(t, client, "myapp", "myapp")
	createFakeDeployment(t, client, "myapp-studio-ui", "myapp")
	createFakeDeployment(t, client, "myapp-studio-api", "myapp")
	createFakeService(t, client, "myapp", "myapp")
	createFakeService(t, client, "myapp-studio-ui", "myapp")
	createFakeService(t, client, "myapp-studio-api", "myapp")
	createFakeIngress(t, client, "myapp", "myapp")

	// Also create a resource for a DIFFERENT project - should not be deleted
	createFakeDeployment(t, client, "other-project", "other-project")
	createFakeService(t, client, "other-project", "other-project")

	if err := d.UndeployAll(ctx, "myapp"); err != nil {
		t.Fatalf("UndeployAll failed: %v", err)
	}

	deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
	if len(deps.Items) != 1 {
		t.Errorf("expected 1 deployment (other-project), got %d", len(deps.Items))
	}
	if len(deps.Items) == 1 && deps.Items[0].Name != "other-project" {
		t.Errorf("remaining deployment = %q, want %q", deps.Items[0].Name, "other-project")
	}

	svcs, _ := client.CoreV1().Services("projects").List(ctx, metav1.ListOptions{})
	if len(svcs.Items) != 1 {
		t.Errorf("expected 1 service (other-project), got %d", len(svcs.Items))
	}

	ings, _ := client.NetworkingV1().Ingresses("projects").List(ctx, metav1.ListOptions{})
	if len(ings.Items) != 0 {
		t.Errorf("expected 0 ingresses, got %d", len(ings.Items))
	}
}

func TestUndeployAll_IdempotentNoOp(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployer(client)
	ctx := context.Background()

	// No resources exist for this project
	if err := d.UndeployAll(ctx, "nonexistent"); err != nil {
		t.Fatalf("UndeployAll should be idempotent, got: %v", err)
	}
}

func TestListProjectLabels(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployer(client)
	ctx := context.Background()

	createFakeDeployment(t, client, "proj-a", "proj-a")
	createFakeDeployment(t, client, "proj-b", "proj-b")
	createFakeDeployment(t, client, "proj-b-ui", "proj-b")
	createFakeDeployment(t, client, "proj-b-api", "proj-b")

	labels, err := d.ListProjectLabels(ctx)
	if err != nil {
		t.Fatalf("ListProjectLabels failed: %v", err)
	}

	// Should have exactly 2 unique labels
	if len(labels) != 2 {
		t.Fatalf("expected 2 unique labels, got %d: %v", len(labels), labels)
	}

	found := make(map[string]bool)
	for _, l := range labels {
		found[l] = true
	}
	if !found["proj-a"] || !found["proj-b"] {
		t.Errorf("expected proj-a and proj-b, got %v", labels)
	}
}

func TestListProjectLabels_Empty(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployer(client)

	labels, err := d.ListProjectLabels(context.Background())
	if err != nil {
		t.Fatalf("ListProjectLabels failed: %v", err)
	}
	if len(labels) != 0 {
		t.Errorf("expected 0 labels, got %d", len(labels))
	}
}

func TestGetOldestResourceTime(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployer(client)
	ctx := context.Background()

	createFakeDeployment(t, client, "myapp", "myapp")
	createFakeDeployment(t, client, "myapp-ui", "myapp")

	_, found, err := d.GetOldestResourceTime(ctx, "myapp")
	if err != nil {
		t.Fatalf("GetOldestResourceTime failed: %v", err)
	}
	if !found {
		t.Fatal("expected found=true")
	}
}

func TestGetOldestResourceTime_NoResources(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployer(client)

	_, found, err := d.GetOldestResourceTime(context.Background(), "nonexistent")
	if err != nil {
		t.Fatalf("GetOldestResourceTime failed: %v", err)
	}
	if found {
		t.Error("expected found=false for nonexistent project")
	}
}
