package zddc

import (
	"os"
	"path/filepath"
	"testing"
)

func TestIsAdmin(t *testing.T) {
	cases := []struct {
		name     string
		zddcBody string // contents of <root>/.zddc; empty string means no file
		email    string
		want     bool
	}{
		{
			name:     "no zddc file → not admin",
			zddcBody: "",
			email:    "alice@example.com",
			want:     false,
		},
		{
			name:     "zddc file with no admins key → not admin",
			zddcBody: "acl:\n  permissions:\n    \"*\": rwcd\n",
			email:    "alice@example.com",
			want:     false,
		},
		{
			name:     "zddc file with empty admins list → not admin",
			zddcBody: "admins: []\n",
			email:    "alice@example.com",
			want:     false,
		},
		{
			name:     "exact-match admin → admin",
			zddcBody: "admins:\n  - alice@example.com\n",
			email:    "alice@example.com",
			want:     true,
		},
		{
			name:     "domain glob admin → admin",
			zddcBody: "admins:\n  - \"*@example.com\"\n",
			email:    "alice@example.com",
			want:     true,
		},
		{
			name:     "domain glob admin, wrong domain → not admin",
			zddcBody: "admins:\n  - \"*@example.com\"\n",
			email:    "alice@other.org",
			want:     false,
		},
		{
			name:     "non-matching listed admin → not admin",
			zddcBody: "admins:\n  - bob@example.com\n",
			email:    "alice@example.com",
			want:     false,
		},
		{
			name:     "empty email never matches even if pattern is *",
			zddcBody: "admins:\n  - \"*\"\n",
			email:    "",
			want:     false,
		},
		{
			name:     "acl deny does not affect admins",
			zddcBody: "acl:\n  permissions:\n    \"*@example.com\": \"\"\nadmins:\n  - alice@example.com\n",
			email:    "alice@example.com",
			want:     true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			root := t.TempDir()
			if tc.zddcBody != "" {
				if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte(tc.zddcBody), 0o644); err != nil {
					t.Fatalf("write .zddc: %v", err)
				}
			}
			if got := IsAdmin(root, Principal{Email: tc.email, Elevated: true}); got != tc.want {
				t.Errorf("IsAdmin(%q, %q) = %v, want %v", root, tc.email, got, tc.want)
			}
		})
	}
}

// TestIsAdminSubdirIgnored documents that admins entries in subdirectory
// .zddc files are NOT honored by IsAdmin — only the root .zddc grants the
// server-wide super-admin role. Subtree admin authority is a separate
// concept covered by IsSubtreeAdmin.
func TestIsAdminSubdirIgnored(t *testing.T) {
	root := t.TempDir()
	sub := filepath.Join(root, "project")
	if err := os.MkdirAll(sub, 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}

	// Root has no admins; subdir tries to grant admin.
	if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("acl:\n  permissions:\n    \"*\": rwcd\n"), 0o644); err != nil {
		t.Fatalf("write root .zddc: %v", err)
	}
	if err := os.WriteFile(filepath.Join(sub, ".zddc"), []byte("admins:\n  - mallory@example.com\n"), 0o644); err != nil {
		t.Fatalf("write subdir .zddc: %v", err)
	}

	if IsAdmin(root, Principal{Email: "mallory@example.com", Elevated: true}) {
		t.Error("subdir .zddc admins entry was honored — that is a privilege-escalation hole")
	}
}

// TestIsAdminUnelevated asserts the elevation gate: an admin email
// who hasn't opted into their powers (Elevated:false) is treated as a
// non-admin, even when the root .zddc grants them.
func TestIsAdminUnelevated(t *testing.T) {
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, ".zddc"),
		[]byte("admins:\n  - alice@example.com\n"), 0o644); err != nil {
		t.Fatalf("write .zddc: %v", err)
	}
	if IsAdmin(root, Principal{Email: "alice@example.com", Elevated: false}) {
		t.Error("un-elevated admin reported as IsAdmin — elevation gate broken")
	}
	if !IsAdmin(root, Principal{Email: "alice@example.com", Elevated: true}) {
		t.Error("elevated admin not recognised — gate is too strict")
	}
}

// fixture writes a tree of .zddc files. Keys are paths relative to root;
// the empty string means root itself ("<root>/.zddc"). Values are file
// contents. Intermediate directories are created as needed. Each path is
// joined with ".zddc".
func writeZddcTree(t *testing.T, root string, files map[string]string) {
	t.Helper()
	for rel, body := range files {
		dir := filepath.Join(root, rel)
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatalf("mkdir %s: %v", dir, err)
		}
		// Always invalidate cache before/after writes inside a test so
		// subsequent calls re-read disk. Tests run with t.TempDir() so
		// there's no cross-test contamination, but the in-process cache
		// is global and may carry stale entries between subtests if a
		// prior subtest read the same path.
		InvalidateCache(dir)
		if body == "" {
			continue
		}
		if err := os.WriteFile(filepath.Join(dir, ".zddc"), []byte(body), 0o644); err != nil {
			t.Fatalf("write %s/.zddc: %v", rel, err)
		}
	}
}

func TestIsSubtreeAdmin(t *testing.T) {
	cases := []struct {
		name  string
		files map[string]string
		dir   string // relative to root
		email string
		want  bool
	}{
		{
			name:  "no zddc anywhere → not admin",
			files: map[string]string{},
			dir:   "",
			email: "alice@example.com",
			want:  false,
		},
		{
			name: "root admin → admin of any subtree",
			files: map[string]string{
				"":           "admins:\n  - alice@example.com\n",
				"projects/x": "",
			},
			dir:   "projects/x",
			email: "alice@example.com",
			want:  true,
		},
		{
			name: "subtree admin granted at intermediate level",
			files: map[string]string{
				"":           "admins:\n  - root@example.com\n",
				"projects":   "admins:\n  - alice@example.com\n",
				"projects/x": "",
			},
			dir:   "projects/x",
			email: "alice@example.com",
			want:  true,
		},
		{
			name: "subtree admin granted at the leaf level itself",
			files: map[string]string{
				"":         "admins:\n  - root@example.com\n",
				"projects": "admins:\n  - alice@example.com\n",
			},
			dir:   "projects",
			email: "alice@example.com",
			want:  true,
		},
		{
			name: "non-admin in same subtree → not admin",
			files: map[string]string{
				"":         "admins:\n  - root@example.com\n",
				"projects": "admins:\n  - alice@example.com\n",
			},
			dir:   "projects",
			email: "bob@example.com",
			want:  false,
		},
		{
			name: "admin granted in sibling subtree does not leak",
			files: map[string]string{
				"":    "admins:\n  - root@example.com\n",
				"foo": "admins:\n  - alice@example.com\n",
				"bar": "",
			},
			dir:   "bar",
			email: "alice@example.com",
			want:  false,
		},
		{
			name: "glob admin",
			files: map[string]string{
				"":         "admins:\n  - \"*@varasys.io\"\n",
				"projects": "",
			},
			dir:   "projects",
			email: "alice@varasys.io",
			want:  true,
		},
		{
			name:  "empty email never admin",
			files: map[string]string{"": "admins:\n  - \"*\"\n"},
			dir:   "",
			email: "",
			want:  false,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			root := t.TempDir()
			writeZddcTree(t, root, tc.files)
			dir := filepath.Join(root, tc.dir)
			if err := os.MkdirAll(dir, 0o755); err != nil {
				t.Fatalf("mkdir target: %v", err)
			}
			InvalidateCache(dir)
			if got := IsSubtreeAdmin(root, dir, Principal{Email: tc.email, Elevated: true}); got != tc.want {
				t.Errorf("IsSubtreeAdmin(%q, %q) = %v, want %v",
					tc.dir, tc.email, got, tc.want)
			}
		})
	}
}

// TestIsAdminForChain pins the unified helper that backs IsAdmin and
// IsSubtreeAdmin. Each table entry covers one property: cascade walk,
// role resolution scope, the bootstrap case for the root file, empty-
// email refusal.
func TestIsAdminForChain(t *testing.T) {
	cases := []struct {
		name  string
		files map[string]string
		dir   string
		email string
		want  bool
	}{
		{
			name:  "root super-admin matches at any depth",
			files: map[string]string{"": "admins:\n  - alice@example.com\n"},
			dir:   "a/b/c",
			email: "alice@example.com",
			want:  true,
		},
		{
			name: "subtree admin matches inside their subtree",
			files: map[string]string{
				"":        "admins:\n  - root@example.com\n",
				"project": "admins:\n  - alice@example.com\n",
			},
			dir:   "project/sub",
			email: "alice@example.com",
			want:  true,
		},
		{
			name: "subtree admin matches at their grant level (no strict-ancestor)",
			files: map[string]string{
				"":        "admins:\n  - root@example.com\n",
				"project": "admins:\n  - alice@example.com\n",
			},
			dir:   "project",
			email: "alice@example.com",
			want:  true,
		},
		{
			name: "subtree admin does NOT match outside their subtree",
			files: map[string]string{
				"":         "admins:\n  - root@example.com\n",
				"project1": "admins:\n  - alice@example.com\n",
			},
			dir:   "project2",
			email: "alice@example.com",
			want:  false,
		},
		{
			name:  "empty email never matches",
			files: map[string]string{"": "admins:\n  - \"*\"\n"},
			dir:   "",
			email: "",
			want:  false,
		},
		{
			name: "role membership in admins entry resolves through cascade",
			files: map[string]string{
				"": "roles:\n  controllers:\n    members: [bob@example.com]\n" +
					"admins:\n  - controllers\n",
			},
			dir:   "",
			email: "bob@example.com",
			want:  true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			root := t.TempDir()
			writeZddcTree(t, root, tc.files)
			dir := filepath.Join(root, tc.dir)
			if err := os.MkdirAll(dir, 0o755); err != nil {
				t.Fatalf("mkdir: %v", err)
			}
			InvalidateCache(dir)
			chain, err := EffectivePolicy(root, dir)
			if err != nil {
				t.Fatalf("EffectivePolicy: %v", err)
			}
			if got := IsAdminForChain(chain, tc.email); got != tc.want {
				t.Errorf("IsAdminForChain(dir=%q, email=%q) = %v, want %v",
					tc.dir, tc.email, got, tc.want)
			}
		})
	}
}

// TestAdminLevelInChain pins the level-index contract used by audit
// logging: root match returns 0, subtree match returns the deeper
// index, no match returns -1.
func TestAdminLevelInChain(t *testing.T) {
	cases := []struct {
		name  string
		files map[string]string
		dir   string
		email string
		want  int
	}{
		{
			name:  "root super-admin matches at level 0",
			files: map[string]string{"": "admins:\n  - alice@example.com\n"},
			dir:   "project",
			email: "alice@example.com",
			want:  0,
		},
		{
			name: "subtree admin matches at level 1",
			files: map[string]string{
				"":        "admins:\n  - root@example.com\n",
				"project": "admins:\n  - alice@example.com\n",
			},
			dir:   "project",
			email: "alice@example.com",
			want:  1,
		},
		{
			name: "deeper subtree admin matches at level 2",
			files: map[string]string{
				"":            "admins:\n  - root@example.com\n",
				"project":     "admins:\n  - alice@example.com\n",
				"project/sub": "admins:\n  - bob@example.com\n",
			},
			dir:   "project/sub",
			email: "bob@example.com",
			want:  2,
		},
		{
			name:  "no match returns -1",
			files: map[string]string{"": "admins:\n  - root@example.com\n"},
			dir:   "project",
			email: "stranger@other.org",
			want:  -1,
		},
		{
			name:  "empty email returns -1",
			files: map[string]string{"": "admins:\n  - \"*\"\n"},
			dir:   "",
			email: "",
			want:  -1,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			root := t.TempDir()
			writeZddcTree(t, root, tc.files)
			dir := filepath.Join(root, tc.dir)
			if err := os.MkdirAll(dir, 0o755); err != nil {
				t.Fatalf("mkdir: %v", err)
			}
			InvalidateCache(dir)
			chain, err := EffectivePolicy(root, dir)
			if err != nil {
				t.Fatalf("EffectivePolicy: %v", err)
			}
			if got := AdminLevelInChain(chain, tc.email); got != tc.want {
				t.Errorf("AdminLevelInChain(dir=%q, email=%q) = %d, want %d",
					tc.dir, tc.email, got, tc.want)
			}
		})
	}
}
