package service

import (
	"strings"
	"testing"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tinyauthapp/tinyauth/internal/model"
	"github.com/tinyauthapp/tinyauth/internal/utils/logger"
)

func TestKubernetesService(t *testing.T) {
	log := logger.NewLogger().WithTestConfig()
	log.Init()

	type testCase struct {
		description string
		run         func(t *testing.T, svc *KubernetesService)
	}

	tests := []testCase{
		{
			description: "Cache by domain returns app and misses unknown domain",
			run: func(t *testing.T, svc *KubernetesService) {
				app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
				svc.addIngressEntries(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				}, []ingressEntry{
					{
						app:  app,
						name: "foo",
					},
				})

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "foo.example.com" {
						got = app
						return true
					}
					return false
				})
				require.NotNil(t, got)
				assert.Equal(t, "foo.example.com", got.Config.Domain)
			},
		},
		{
			description: "RemoveIngress clears domain and app name entries",
			run: func(t *testing.T, svc *KubernetesService) {
				app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}}
				svc.addIngressEntries(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				}, []ingressEntry{
					{
						app:  app,
						name: "foo",
					},
				})

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "foo.example.com" {
						got = app
						return true
					}
					return false
				})
				require.NotNil(t, got)
				assert.Equal(t, "foo.example.com", got.Config.Domain)

				got = nil
				svc.removeIngress(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				})

				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "foo.example.com" {
						got = app
						return true
					}
					return false
				})
				assert.Nil(t, got)
			},
		},
		{
			description: "AddIngressApps replaces stale entries for the same ingress",
			run: func(t *testing.T, svc *KubernetesService) {
				old := model.App{Config: model.AppConfig{Domain: "old.example.com"}}
				svc.addIngressEntries(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				}, []ingressEntry{
					{
						app:  old,
						name: "foo",
					},
				})

				updated := model.App{Config: model.AppConfig{Domain: "new.example.com"}}
				svc.addIngressEntries(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				}, []ingressEntry{
					{
						app:  updated,
						name: "foo",
					},
				})

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "old.example.com" {
						got = app
						return true
					}
					return false
				})
				assert.Nil(t, got)

				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "new.example.com" {
						got = app
						return true
					}
					return false
				})
				require.NotNil(t, got)
				assert.Equal(t, "new.example.com", got.Config.Domain)
			},
		},
		{
			description: "GetLabels returns app from cache when connected",
			run: func(t *testing.T, svc *KubernetesService) {
				svc.connected = true

				app := model.App{Config: model.AppConfig{Domain: "hit.example.com"}}
				svc.addIngressEntries(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				}, []ingressEntry{
					{
						app:  app,
						name: "foo",
					},
				})

				var got *model.App
				err := svc.Lookup(func(name string, app *model.App) bool {
					if app.Config.Domain == "hit.example.com" {
						got = app
						return true
					}
					return false
				})
				require.NoError(t, err)
				require.NotNil(t, got)
				assert.Equal(t, "hit.example.com", got.Config.Domain)
			},
		},
		{
			description: "GetLabels returns empty app on cache miss when started",
			run: func(t *testing.T, svc *KubernetesService) {
				svc.connected = true

				var got *model.App
				err := svc.Lookup(func(name string, app *model.App) bool {
					if app.Config.Domain == "notfound.example.com" {
						got = app
						return true
					}
					return false
				})
				require.NoError(t, err)
				require.Nil(t, got)
			},
		},
		{
			description: "GetLabels resolves app by app name",
			run: func(t *testing.T, svc *KubernetesService) {
				svc.connected = true

				app := model.App{Path: model.AppPath{Allow: "/foo"}}
				svc.addIngressEntries(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				}, []ingressEntry{
					{
						app:  app,
						name: "foo",
					},
				})

				var got *model.App
				err := svc.Lookup(func(name string, app *model.App) bool {
					if strings.HasPrefix("foo.internal.example.com", "foo.") {
						got = app
						return true
					}
					return false
				})
				require.NoError(t, err)
				require.NotNil(t, got)
				assert.Equal(t, "/foo", got.Path.Allow)
			},
		},
		{
			description: "GetLabels returns empty app when service not yet started",
			run: func(t *testing.T, svc *KubernetesService) {
				var got *model.App
				err := svc.Lookup(func(name string, app *model.App) bool {
					return false
				})
				require.NoError(t, err)
				assert.Nil(t, got)
			},
		},
		{
			description: "UpdateFromItem parses annotations and populates cache",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.domain": "myapp.example.com",
					"tinyauth.apps.myapp.users.allow":   "alice",
				})
				item.Object["spec"] = map[string]any{
					"rules": []any{
						map[string]any{
							"host": "myapp.example.com",
						},
					},
				}

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "myapp.example.com" {
						got = app
						return true
					}
					return false
				})

				require.NotNil(t, got)
				assert.Equal(t, "myapp.example.com", got.Config.Domain)
				assert.Equal(t, "alice", got.Users.Allow)
			},
		},
		{
			description: "Update from item skips annotations with no hosts",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.domain": "myapp.example.com",
				})

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "myapp.example.com" {
						got = app
						return true
					}
					return false
				})
				assert.Nil(t, got)
			},
		},
		{
			description: "UpdateFromItem fails when label parsing fails",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.domain": "myapp.example.com",
					"tinyauth.apps.myapp.users.break":   "i-dont-exist",
				})
				item.Object["spec"] = map[string]any{
					"rules": []any{
						map[string]any{
							"host": "myapp.example.com",
						},
					},
				}

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "myapp.example.com" {
						got = app
						return true
					}
					return false
				})

				require.Nil(t, got)
			},
		},
		{
			description: "UpdateFromItem with no annotations removes existing cache entries",
			run: func(t *testing.T, svc *KubernetesService) {
				app := model.App{Config: model.AppConfig{Domain: "todelete.example.com"}}
				svc.addIngressEntries(ingressKey{
					namespace: "default",
					name:      "my-ingress",
				}, []ingressEntry{
					{
						app:  app,
						name: "foo",
					},
				})

				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("my-ingress")

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if app.Config.Domain == "todelete.example.com" {
						got = app
						return true
					}
					return false
				})
				assert.Nil(t, got)
			},
		},
		{
			description: "ExtractPaths returns all non empty paths from a rule",
			run: func(t *testing.T, svc *KubernetesService) {
				rule := map[string]any{
					"http": map[string]any{
						"paths": []any{
							map[string]any{"path": "/"},
							map[string]any{"path": "/api"},
							map[string]any{"path": ""},
							map[string]any{"pathType": "Prefix"},
							"not-a-map",
						},
					},
				}

				paths, err := svc.extractPaths(rule)
				require.NoError(t, err)
				assert.Equal(t, []string{"/", "/api"}, paths)
			},
		},
		{
			description: "ExtractPaths returns nothing when http or paths are missing",
			run: func(t *testing.T, svc *KubernetesService) {
				paths, err := svc.extractPaths(map[string]any{})
				require.NoError(t, err)
				assert.Empty(t, paths)

				paths, err = svc.extractPaths(map[string]any{
					"http": map[string]any{},
				})
				require.NoError(t, err)
				assert.Empty(t, paths)
			},
		},
		{
			description: "ExtractPaths errors when http is not a map",
			run: func(t *testing.T, svc *KubernetesService) {
				paths, err := svc.extractPaths(map[string]any{
					"http": "invalid",
				})
				require.Error(t, err)
				assert.Nil(t, paths)
			},
		},
		{
			description: "ExtractPaths errors when paths is not a slice",
			run: func(t *testing.T, svc *KubernetesService) {
				paths, err := svc.extractPaths(map[string]any{
					"http": map[string]any{
						"paths": "invalid",
					},
				})
				require.Error(t, err)
				assert.Nil(t, paths)
			},
		},
		{
			description: "ExtractHosts returns hosts from all rules",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
					map[string]any{
						"host": "foo.example.com",
						"http": map[string]any{
							"paths": []any{
								map[string]any{"path": "/"},
							},
						},
					},
					map[string]any{
						"host": "bar.example.com",
					},
					map[string]any{
						"host": "",
					},
					"not-a-map",
				}, "spec", "rules"))

				hosts, err := svc.extractHosts(&item)
				require.NoError(t, err)
				assert.Equal(t, []string{"foo.example.com", "bar.example.com"}, hosts)
			},
		},
		{
			description: "ExtractHosts still returns hosts when a rule has no catch all path",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
					map[string]any{
						"host": "foo.example.com",
						"http": map[string]any{
							"paths": []any{
								map[string]any{"path": "/api"},
							},
						},
					},
				}, "spec", "rules"))

				hosts, err := svc.extractHosts(&item)
				require.NoError(t, err)
				assert.Equal(t, []string{"foo.example.com"}, hosts)
			},
		},
		{
			description: "ExtractHosts still returns hosts when path extraction fails",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
					map[string]any{
						"host": "foo.example.com",
						"http": "invalid",
					},
				}, "spec", "rules"))

				hosts, err := svc.extractHosts(&item)
				require.NoError(t, err)
				assert.Equal(t, []string{"foo.example.com"}, hosts)
			},
		},
		{
			description: "ExtractHosts returns nothing when spec.rules is missing",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")

				hosts, err := svc.extractHosts(&item)
				require.NoError(t, err)
				assert.Empty(t, hosts)
			},
		},
		{
			description: "ExtractHosts errors when spec.rules is not a slice",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules"))

				hosts, err := svc.extractHosts(&item)
				require.Error(t, err)
				assert.Nil(t, hosts)
			},
		},
		{
			description: "UpdateFromItem registers app when its domain matches an ingress host",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.domain": "myapp.example.com",
				})
				require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
					map[string]any{
						"host": "myapp.example.com",
					},
				}, "spec", "rules"))

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if name == "myapp" {
						got = app
						return true
					}
					return false
				})
				require.NotNil(t, got)
				assert.Equal(t, "myapp.example.com", got.Config.Domain)
			},
		},
		{
			description: "UpdateFromItem registers app when its name matches an ingress host prefix",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.users.allow": "alice",
				})
				require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
					map[string]any{
						"host": "MyApp.example.com",
					},
				}, "spec", "rules"))

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if name == "myapp" {
						got = app
						return true
					}
					return false
				})
				require.NotNil(t, got)
				assert.Equal(t, "alice", got.Users.Allow)
			},
		},
		{
			description: "UpdateFromItem skips apps that match neither host nor name",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.domain": "myapp.example.com",
				})
				require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
					map[string]any{
						"host": "other.example.com",
					},
				}, "spec", "rules"))

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					got = app
					return true
				})
				assert.Nil(t, got)
			},
		},
		{
			description: "UpdateFromItem falls back to app name when the domain is invalid",
			run: func(t *testing.T, svc *KubernetesService) {
				item := unstructured.Unstructured{}
				item.SetNamespace("default")
				item.SetName("test-ingress")
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.domain": "not a domain",
				})
				require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{
					map[string]any{
						"host": "myapp.example.com",
					},
				}, "spec", "rules"))

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					if name == "myapp" {
						got = app
						return true
					}
					return false
				})
				require.NotNil(t, got)
			},
		},
		{
			description: "UpdateFromItem removes entries when host extraction fails",
			run: func(t *testing.T, svc *KubernetesService) {
				key := ingressKey{
					namespace: "default",
					name:      "test-ingress",
				}
				svc.addIngressEntries(key, []ingressEntry{
					{
						app:  model.App{Config: model.AppConfig{Domain: "stale.example.com"}},
						name: "foo",
					},
				})

				item := unstructured.Unstructured{}
				item.SetNamespace(key.namespace)
				item.SetName(key.name)
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.domain": "myapp.example.com",
				})
				require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules"))

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					got = app
					return true
				})
				assert.Nil(t, got)
			},
		},
		{
			description: "UpdateFromItem removes entries when annotations are not decodable",
			run: func(t *testing.T, svc *KubernetesService) {
				key := ingressKey{
					namespace: "default",
					name:      "test-ingress",
				}
				svc.addIngressEntries(key, []ingressEntry{
					{
						app:  model.App{Config: model.AppConfig{Domain: "stale.example.com"}},
						name: "foo",
					},
				})

				item := unstructured.Unstructured{}
				item.SetNamespace(key.namespace)
				item.SetName(key.name)
				item.SetAnnotations(map[string]string{
					"tinyauth.apps.myapp.config.oauthWhitelist": "[",
				})

				svc.updateFromItem(&item)

				var got *model.App
				svc.getEntry(func(name string, app *model.App) bool {
					got = app
					return true
				})
				assert.Nil(t, got)
			},
		},
	}

	for _, test := range tests {
		t.Run(test.description, func(t *testing.T) {
			svc := &KubernetesService{
				ingressEntries: make(map[ingressKey][]ingressEntry),
				log:            log,
			}
			test.run(t, svc)
		})
	}
}
