package libkubectl

import (
	"os"
	"strings"
	"testing"

	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/dynamic/fake"
	k8stesting "k8s.io/client-go/testing"
)

// TestApplyDynamic tests require a Kubernetes cluster.
//
// Running the tests:
//   - With cluster: go test -v ./pkg/libkubectl -run TestApplyDynamic
//   - Without cluster: Tests will skip automatically
//
// Cleanup: Tests automatically clean up resources using client.DeleteDynamic().
// Resources are deleted after each test completes, keeping the cluster clean.

// skipIfNoKubeconfig skips the test if no kubeconfig is available
func skipIfNoKubeconfig(tb testing.TB) string {
	tb.Helper()

	// Check for kubeconfig in environment variable
	kubeconfig := os.Getenv("KUBECONFIG")
	if kubeconfig == "" {
		// Try default location
		homeDir, err := os.UserHomeDir()
		if err != nil {
			tb.Skip("No KUBECONFIG environment variable set and cannot determine home directory")
		}
		kubeconfig = homeDir + "/.kube/config"
	}

	// Check if kubeconfig file exists
	if _, err := os.Stat(kubeconfig); os.IsNotExist(err) {
		tb.Skip("No Kubernetes cluster available (kubeconfig not found). Set KUBECONFIG environment variable to run these tests")
	}

	return kubeconfig
}

func TestApplyDynamic(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name      string
		manifests []string
		wantErr   bool
		errMsg    string
		desc      string
	}{
		{
			name: "apply simple deployment",
			desc: "Test basic deployment resource application",
			manifests: []string{
				`apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
      - name: test
        image: nginx:latest`,
			},
			wantErr: false,
		},
		{
			name: "apply statefulset",
			desc: "Test StatefulSet resource application",
			manifests: []string{
				`apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: test-statefulset
  namespace: default
spec:
  serviceName: test-service
  replicas: 2
  selector:
    matchLabels:
      app: test-stateful
  template:
    metadata:
      labels:
        app: test-stateful
    spec:
      containers:
      - name: test
        image: nginx:latest`,
			},
			wantErr: false,
		},
		{
			name: "apply daemonset",
			desc: "Test DaemonSet resource application",
			manifests: []string{
				`apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: test-daemonset
  namespace: default
spec:
  selector:
    matchLabels:
      app: test-daemon
  template:
    metadata:
      labels:
        app: test-daemon
    spec:
      containers:
      - name: test
        image: nginx:latest`,
			},
			wantErr: false,
		},
		{
			name: "apply service",
			desc: "Test Service resource application",
			manifests: []string{
				`apiVersion: v1
kind: Service
metadata:
  name: test-service
  namespace: default
spec:
  selector:
    app: test
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080`,
			},
			wantErr: false,
		},
		{
			name: "apply ingress",
			desc: "Test Ingress resource application",
			manifests: []string{
				`apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  namespace: default
spec:
  rules:
  - host: test.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: test-service
            port:
              number: 80`,
			},
			wantErr: false,
		},
		{
			name: "apply configmap and secret",
			desc: "Test ConfigMap and Secret resource application",
			manifests: []string{
				`apiVersion: v1
kind: ConfigMap
metadata:
  name: test-config
  namespace: default
data:
  key1: value1
  key2: value2
---
apiVersion: v1
kind: Secret
metadata:
  name: test-secret
  namespace: default
type: Opaque
stringData:
  username: admin
  password: secret123`,
			},
			wantErr: false,
		},
		{
			name: "apply persistent volume claim",
			desc: "Test PersistentVolumeClaim resource application",
			manifests: []string{
				`apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
  namespace: default
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi`,
			},
			wantErr: false,
		},
		{
			name: "apply serviceaccount with rbac",
			desc: "Test ServiceAccount, Role, and RoleBinding resources",
			manifests: []string{
				`apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-sa
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-role
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-rolebinding
  namespace: default
subjects:
- kind: ServiceAccount
  name: test-sa
  namespace: default
roleRef:
  kind: Role
  name: test-role
  apiGroup: rbac.authorization.k8s.io`,
			},
			wantErr: false,
		},
		{
			name: "apply cluster-scoped resources",
			desc: "Test cluster-scoped resources (ClusterRole, ClusterRoleBinding)",
			manifests: []string{
				`apiVersion: v1
kind: Namespace
metadata:
  name: test-namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-cluster-role
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-cluster-rolebinding
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
roleRef:
  kind: ClusterRole
  name: test-cluster-role
  apiGroup: rbac.authorization.k8s.io`,
			},
			wantErr: false,
		},
		{
			name: "apply job and cronjob",
			desc: "Test Job and CronJob resources",
			manifests: []string{
				`apiVersion: batch/v1
kind: Job
metadata:
  name: test-job
  namespace: default
spec:
  template:
    spec:
      containers:
      - name: test
        image: busybox
        command: ["echo", "Hello"]
      restartPolicy: Never
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: test-cronjob
  namespace: default
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: test
            image: busybox
            command: ["echo", "Scheduled"]
          restartPolicy: Never`,
			},
			wantErr: false,
		},
		{
			name: "apply network policy",
			desc: "Test NetworkPolicy resource application",
			manifests: []string{
				`apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: test
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080`,
			},
			wantErr: false,
		},
		{
			name: "apply horizontal pod autoscaler",
			desc: "Test HorizontalPodAutoscaler resource application",
			manifests: []string{
				`apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: test-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: test-deployment
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 80`,
			},
			wantErr: false,
		},
		{
			name: "apply full application stack",
			desc: "Test complete application with multiple resources",
			manifests: []string{
				`apiVersion: v1
kind: Namespace
metadata:
  name: test-app
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: test-app
data:
  DATABASE_URL: postgresql://db:5432
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secret
  namespace: test-app
type: Opaque
stringData:
  db-password: supersecret
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
  namespace: test-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: nginx:latest
        envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secret
---
apiVersion: v1
kind: Service
metadata:
  name: app-service
  namespace: test-app
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080`,
			},
			wantErr: false,
		},
		{
			name: "apply multiple manifests separately",
			desc: "Test applying multiple manifests in separate strings",
			manifests: []string{
				`apiVersion: v1
kind: Namespace
metadata:
  name: multi-test`,
				`apiVersion: v1
kind: ConfigMap
metadata:
  name: config1
  namespace: multi-test
data:
  key: value1`,
				`apiVersion: v1
kind: ConfigMap
metadata:
  name: config2
  namespace: multi-test
data:
  key: value2`,
			},
			wantErr: false,
		},
		{
			name: "empty manifest",
			desc: "Test empty manifest handling",
			manifests: []string{
				"",
			},
			wantErr: false,
		},
		{
			name: "multiple empty documents",
			desc: "Test multiple empty documents separated by ---",
			manifests: []string{
				`---
---
---`,
			},
			wantErr: false,
		},
		{
			name: "invalid yaml",
			desc: "Test invalid YAML syntax handling",
			manifests: []string{
				`invalid: yaml: content: [unclosed`,
			},
			wantErr: true,
			errMsg:  "failed to decode YAML",
		},
		{
			name: "missing required fields",
			desc: "Test manifest with missing kind field",
			manifests: []string{
				`apiVersion: v1
metadata:
  name: test`,
			},
			wantErr: true,
			errMsg:  "failed to decode YAML", // Kubernetes returns decode error for missing required fields
		},
		{
			name: "partial failure with multiple resources",
			desc: "Test partial application with some invalid resources",
			manifests: []string{
				`apiVersion: v1
kind: ConfigMap
metadata:
  name: valid-config
  namespace: default
data:
  key: value
---
invalid: yaml: [unclosed
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: another-valid-config
  namespace: default
data:
  key: value2`,
			},
			wantErr: true,
			errMsg:  "partially applied resources with errors",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Skip if no Kubernetes cluster is available
			kubeconfig := skipIfNoKubeconfig(t)

			// Create client with empty namespace to let manifest namespaces be used
			// (simulates "use namespace from manifest" toggle being ON)
			client, err := NewClient(&ClientAccess{}, "", kubeconfig, false)
			if err != nil {
				t.Fatalf("Failed to create client: %v", err)
			}

			// Register cleanup to delete resources after test completes
			// This runs even if the test fails, ensuring cluster stays clean
			if !tt.wantErr && len(tt.manifests) > 0 {
				t.Cleanup(func() {
					_, err := client.DeleteDynamic(t.Context(), tt.manifests)
					if err != nil {
						t.Logf("Warning: failed to cleanup resources: %v", err)
					}
				})
			}

			output, err := client.ApplyDynamic(t.Context(), tt.manifests)

			if tt.wantErr {
				if err == nil {
					t.Errorf("ApplyDynamic() expected error but got none")
				} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf("ApplyDynamic() error = %v, want error containing %v", err, tt.errMsg)
				}
			} else {
				if err != nil {
					t.Errorf("ApplyDynamic() unexpected error = %v", err)
				}
				if output == "" && len(tt.manifests) > 0 && tt.manifests[0] != "" {
					t.Errorf("ApplyDynamic() expected output but got empty string")
				}
			}
		})
	}
}

// newFakeDynamicClient creates a fake dynamic client that handles Server-Side Apply
func newFakeDynamicClient() *fake.FakeDynamicClient {
	client := fake.NewSimpleDynamicClient(runtime.NewScheme())
	client.PrependReactor("patch", "*", func(action k8stesting.Action) (bool, runtime.Object, error) {
		patchAction := action.(k8stesting.PatchAction)
		if patchAction.GetPatchType() == types.ApplyPatchType {
			obj := &unstructured.Unstructured{}
			if err := obj.UnmarshalJSON(patchAction.GetPatch()); err != nil {
				return true, nil, err
			}
			return true, obj, nil
		}
		return false, nil, nil
	})
	return client
}

// newTestMapper creates a simple RESTMapper for common resource types
func newTestMapper() meta.RESTMapper {
	mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{
		{Group: "", Version: "v1"},
		{Group: "apps", Version: "v1"},
	})
	mapper.Add(schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, meta.RESTScopeNamespace)
	mapper.Add(schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, meta.RESTScopeNamespace)
	mapper.Add(schema.GroupVersionKind{Version: "v1", Kind: "Namespace"}, meta.RESTScopeRoot)
	mapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace)
	return mapper
}

// TestApplyResource unit tests for applyResource using fake client (no cluster needed)
func TestApplyResource(t *testing.T) {
	t.Parallel()
	client := &Client{} // applyResource doesn't use Client fields directly
	dynamicClient := newFakeDynamicClient()
	mapper := newTestMapper()

	tests := []struct {
		name         string
		yaml         string
		configuredNS string
		wantErr      bool
		errContains  string
	}{
		{
			name: "configmap with configured namespace",
			yaml: `apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
data:
  key: value`,
			configuredNS: "my-ns",
		},
		{
			name: "configmap with manifest namespace",
			yaml: `apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
  namespace: manifest-ns
data:
  key: value`,
			configuredNS: "",
		},
		{
			name: "namespace conflict",
			yaml: `apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
  namespace: manifest-ns`,
			configuredNS: "form-ns",
			wantErr:      true,
			errContains:  "namespace conflict",
		},
		{
			name: "namespaces match",
			yaml: `apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
  namespace: same-ns`,
			configuredNS: "same-ns",
		},
		{
			name: "defaults to default namespace when neither set",
			yaml: `apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm`,
			configuredNS: "",
		},
		{
			name: "cluster-scoped resource ignores namespace",
			yaml: `apiVersion: v1
kind: Namespace
metadata:
  name: new-ns`,
			configuredNS: "ignored",
		},
		{
			name:        "invalid yaml",
			yaml:        "not: valid: yaml: {{",
			wantErr:     true,
			errContains: "failed to decode",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := client.applyResource(t.Context(), dynamicClient, mapper, []byte(tt.yaml), tt.configuredNS)

			if tt.wantErr {
				if err == nil {
					t.Errorf("expected error containing %q, got nil", tt.errContains)
				} else if !strings.Contains(err.Error(), tt.errContains) {
					t.Errorf("expected error containing %q, got %v", tt.errContains, err)
				}
				return
			}
			if err != nil {
				t.Errorf("unexpected error: %v", err)
				return
			}
			if result == "" {
				t.Error("expected non-empty result")
			}
		})
	}
}

func TestIsManifestFile(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name     string
		resource string
		want     bool
	}{
		{
			name:     "yaml file",
			resource: "deployment.yaml",
			want:     true,
		},
		{
			name:     "yml file",
			resource: "deployment.yml",
			want:     true,
		},
		{
			name:     "yaml file with path",
			resource: "/path/to/deployment.yaml",
			want:     true,
		},
		{
			name:     "yaml file with spaces",
			resource: "  deployment.yaml  ",
			want:     true,
		},
		{
			name:     "not a file",
			resource: "apiVersion: v1\nkind: Pod",
			want:     false,
		},
		{
			name:     "resource type",
			resource: "deployment/my-deployment",
			want:     false,
		},
		{
			name:     "empty string",
			resource: "",
			want:     false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := isManifestFile(tt.resource); got != tt.want {
				t.Errorf("isManifestFile() = %v, want %v", got, tt.want)
			}
		})
	}
}
