package worker

import (
	"context"
	"testing"
	"time"

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

	"github.com/orchard9/rdev/internal/adapter/deployer"
)

// mockProjectChecker simulates DB project existence checks.
type mockProjectChecker struct {
	projects map[string]bool
}

func (m *mockProjectChecker) projectExists(_ context.Context, projectID string) (bool, error) {
	return m.projects[projectID], nil
}

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

func createDeployment(t *testing.T, client *fake.Clientset, name, project string, createdAt time.Time) {
	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},
			CreationTimestamp: metav1.NewTime(createdAt),
		},
		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 TestResourceGC_DeletesOrphans(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployerForGC(client)
	ctx := context.Background()

	// Create resources for two projects, both old enough
	oldTime := time.Now().Add(-2 * time.Hour)
	createDeployment(t, client, "orphan-proj", "orphan-proj", oldTime)
	createDeployment(t, client, "valid-proj", "valid-proj", oldTime)

	checker := &mockProjectChecker{
		projects: map[string]bool{
			"valid-proj": true, // exists in DB
			// orphan-proj is NOT in DB
		},
	}

	gc := newResourceGCWithChecker(d, checker, &ResourceGCConfig{
		MinAge:            1 * time.Hour,
		ReconcileInterval: 1 * time.Hour, // won't tick during test
	})

	gc.runReconciliation()

	// Orphan should be deleted
	deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
	if len(deps.Items) != 1 {
		t.Fatalf("expected 1 deployment, got %d", len(deps.Items))
	}
	if deps.Items[0].Name != "valid-proj" {
		t.Errorf("remaining deployment = %q, want %q", deps.Items[0].Name, "valid-proj")
	}
}

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

	oldTime := time.Now().Add(-2 * time.Hour)
	createDeployment(t, client, "proj-a", "proj-a", oldTime)
	createDeployment(t, client, "proj-b", "proj-b", oldTime)

	checker := &mockProjectChecker{
		projects: map[string]bool{
			"proj-a": true,
			"proj-b": true,
		},
	}

	gc := newResourceGCWithChecker(d, checker, &ResourceGCConfig{
		MinAge:            1 * time.Hour,
		ReconcileInterval: 1 * time.Hour,
	})

	gc.runReconciliation()

	// Both should remain
	deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
	if len(deps.Items) != 2 {
		t.Errorf("expected 2 deployments, got %d", len(deps.Items))
	}
}

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

	// Create a recent deployment (younger than minAge)
	recentTime := time.Now().Add(-5 * time.Minute)
	createDeployment(t, client, "new-orphan", "new-orphan", recentTime)

	checker := &mockProjectChecker{
		projects: map[string]bool{}, // not in DB
	}

	gc := newResourceGCWithChecker(d, checker, &ResourceGCConfig{
		MinAge:            1 * time.Hour,
		ReconcileInterval: 1 * time.Hour,
	})

	gc.runReconciliation()

	// Should NOT be deleted (too young)
	deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
	if len(deps.Items) != 1 {
		t.Errorf("expected 1 deployment (young orphan preserved), got %d", len(deps.Items))
	}
}

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

	oldTime := time.Now().Add(-2 * time.Hour)
	createDeployment(t, client, "myapp", "myapp", oldTime)
	createDeployment(t, client, "myapp-studio-ui", "myapp", oldTime)
	createDeployment(t, client, "myapp-studio-api", "myapp", oldTime)

	checker := &mockProjectChecker{
		projects: map[string]bool{}, // not in DB
	}

	gc := newResourceGCWithChecker(d, checker, &ResourceGCConfig{
		MinAge:            1 * time.Hour,
		ReconcileInterval: 1 * time.Hour,
	})

	gc.runReconciliation()

	// All 3 should be deleted
	deps, _ := client.AppsV1().Deployments("projects").List(ctx, metav1.ListOptions{})
	if len(deps.Items) != 0 {
		t.Errorf("expected 0 deployments, got %d", len(deps.Items))
	}
}

func TestResourceGC_StartStop(t *testing.T) {
	client := fake.NewSimpleClientset()
	d := newTestDeployerForGC(client)

	checker := &mockProjectChecker{projects: map[string]bool{}}

	gc := newResourceGCWithChecker(d, checker, &ResourceGCConfig{
		MinAge:            1 * time.Hour,
		ReconcileInterval: 24 * time.Hour, // long interval so ticker won't fire
	})

	gc.Start()

	// Give the goroutine time to run initial reconciliation
	time.Sleep(50 * time.Millisecond)

	gc.Stop() // should not hang
}
