package deployer

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes/fake"
	k8stesting "k8s.io/client-go/testing"

	"github.com/kubeasy-dev/kubeasy-cli/internal/kube"
)

// NOTE: tests that swap kube.ApplyManifest must NOT run in parallel.

func TestApplyManifestDirs_FindsNestedYAML(t *testing.T) {
	// applyManifestDirs should walk manifests/ recursively and apply every .yaml file.
	tmpDir := t.TempDir()
	manifestsDir := filepath.Join(tmpDir, "manifests")
	require.NoError(t, os.MkdirAll(filepath.Join(manifestsDir, "subdir"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(manifestsDir, "deployment.yaml"), []byte("kind: Deployment"), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(manifestsDir, "subdir", "service.yaml"), []byte("kind: Service"), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(manifestsDir, "readme.txt"), []byte("ignore"), 0o600))

	var applied []string
	old := kube.ApplyManifest
	defer func() { kube.ApplyManifest = old }()
	kube.ApplyManifest = func(_ context.Context, data []byte, _ string, _ meta.RESTMapper, _ dynamic.Interface) error {
		applied = append(applied, strings.TrimSpace(string(data)))
		return nil
	}

	require.NoError(t, applyManifestDirs(context.Background(), tmpDir, "test-ns", nil, nil))
	assert.Len(t, applied, 2)
	assert.Contains(t, applied, "kind: Deployment")
	assert.Contains(t, applied, "kind: Service")
}

func TestChallengesOCIReference(t *testing.T) {
	slugs := []string{"pod-evicted", "first-deployment", "env-config"}

	for _, slug := range slugs {
		ref := fmt.Sprintf("%s/%s:latest", ChallengesOCIRegistry, slug)
		assert.Contains(t, ref, "ghcr.io/", "OCI reference should use GitHub Container Registry")
		assert.True(t, strings.HasSuffix(ref, ":latest"), "OCI reference should use :latest tag")
		assert.Contains(t, ref, slug, "OCI reference should contain the challenge slug")
	}
}

func TestChallengesOCIRegistryFormat(t *testing.T) {
	assert.True(t, strings.HasPrefix(ChallengesOCIRegistry, "ghcr.io/"),
		"OCI registry should be on ghcr.io")
	assert.False(t, strings.HasSuffix(ChallengesOCIRegistry, "/"),
		"OCI registry should not have trailing slash")
}

func TestApplyManifestDirs_SkipsNonYAML(t *testing.T) {
	// applyManifestDirs must only apply .yaml/.yml files; other extensions are ignored.
	tmpDir := t.TempDir()
	manifestsDir := filepath.Join(tmpDir, "manifests")
	require.NoError(t, os.MkdirAll(manifestsDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(manifestsDir, "deploy.yaml"), []byte("yaml"), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(manifestsDir, "svc.yml"), []byte("yml"), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(manifestsDir, "notes.txt"), []byte("txt"), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(manifestsDir, "config.json"), []byte("json"), 0o600))

	var applied []string
	old := kube.ApplyManifest
	defer func() { kube.ApplyManifest = old }()
	kube.ApplyManifest = func(_ context.Context, data []byte, _ string, _ meta.RESTMapper, _ dynamic.Interface) error {
		applied = append(applied, strings.TrimSpace(string(data)))
		return nil
	}

	require.NoError(t, applyManifestDirs(context.Background(), tmpDir, "test-ns", nil, nil))
	assert.Len(t, applied, 2)
	assert.Contains(t, applied, "yaml")
	assert.Contains(t, applied, "yml")
}

func TestApplyManifestDirs_MissingDirectory(t *testing.T) {
	// applyManifestDirs must silently skip a missing manifests/ or policies/ dir.
	tmpDir := t.TempDir() // no manifests/ or policies/ subdirs created

	old := kube.ApplyManifest
	defer func() { kube.ApplyManifest = old }()
	called := false
	kube.ApplyManifest = func(_ context.Context, _ []byte, _ string, _ meta.RESTMapper, _ dynamic.Interface) error {
		called = true
		return nil
	}

	require.NoError(t, applyManifestDirs(context.Background(), tmpDir, "test-ns", nil, nil))
	assert.False(t, called, "ApplyManifest should not be called when directories are missing")
}

func TestApplyManifestDirs_EmptyDirectory(t *testing.T) {
	// applyManifestDirs must succeed with zero apply calls when manifests/ is empty.
	tmpDir := t.TempDir()
	require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "manifests"), 0o755))

	old := kube.ApplyManifest
	defer func() { kube.ApplyManifest = old }()
	called := false
	kube.ApplyManifest = func(_ context.Context, _ []byte, _ string, _ meta.RESTMapper, _ dynamic.Interface) error {
		called = true
		return nil
	}

	require.NoError(t, applyManifestDirs(context.Background(), tmpDir, "test-ns", nil, nil))
	assert.False(t, called)
}

func TestWaitForChallengeReady(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	ns := "test-challenge"

	t.Run("success with ready deployments", func(t *testing.T) {
		clientset := fake.NewClientset(
			&appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{Name: "dep1", Namespace: ns, Generation: 1},
				Spec:       appsv1.DeploymentSpec{Replicas: int32Ptr(1)},
				Status: appsv1.DeploymentStatus{
					ObservedGeneration: 1,
					UpdatedReplicas:    1,
					AvailableReplicas:  1,
					ReadyReplicas:      1,
					Replicas:           1,
				},
			},
		)
		err := WaitForChallengeReady(ctx, clientset, ns)
		assert.NoError(t, err)
	})

	t.Run("empty namespace passes immediately", func(t *testing.T) {
		clientset := fake.NewClientset()
		err := WaitForChallengeReady(ctx, clientset, ns)
		assert.NoError(t, err)
	})

	t.Run("list error fails", func(t *testing.T) {
		clientset := fake.NewClientset()
		clientset.PrependReactor("list", "deployments", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
			return true, nil, fmt.Errorf("api error")
		})
		err := WaitForChallengeReady(ctx, clientset, ns)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "api error")
	})
}

func TestWaitForChallengeReady_StatefulSets(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	ns := "test-challenge"

	t.Run("success with ready statefulsets", func(t *testing.T) {
		clientset := fake.NewClientset(
			&appsv1.StatefulSet{
				ObjectMeta: metav1.ObjectMeta{Name: "sts1", Namespace: ns, Generation: 1},
				Spec:       appsv1.StatefulSetSpec{Replicas: int32Ptr(1)},
				Status: appsv1.StatefulSetStatus{
					ObservedGeneration: 1,
					ReadyReplicas:      1,
					UpdatedReplicas:    1,
					Replicas:           1,
				},
			},
		)
		err := WaitForChallengeReady(ctx, clientset, ns)
		assert.NoError(t, err)
	})

	t.Run("list error fails for statefulsets", func(t *testing.T) {
		clientset := fake.NewClientset()
		clientset.PrependReactor("list", "statefulsets", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
			return true, nil, fmt.Errorf("sts api error")
		})
		err := WaitForChallengeReady(ctx, clientset, ns)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "sts api error")
	})
}

func int32Ptr(i int32) *int32 { return &i }
