package handler

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
	"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)

// zddcTestSetup writes a tree of .zddc files and returns the root and a
// helper that builds requests with an injected user email. files keys
// are paths relative to root; the empty string is the root itself. Each
// path is created as a directory; if the value is non-empty it is
// written as that directory's .zddc.
func zddcTestSetup(t *testing.T, files map[string]string) (cfg config.Config, do func(method, target, email, body string) *httptest.ResponseRecorder) {
	t.Helper()
	root := t.TempDir()
	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)
		}
		zddc.InvalidateCache(dir)
		if body == "" {
			continue
		}
		if err := os.WriteFile(filepath.Join(dir, ".zddc"), []byte(body), 0o644); err != nil {
			t.Fatalf("write .zddc: %v", err)
		}
	}
	cfg = config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	do = func(method, target, email, body string) *httptest.ResponseRecorder {
		var rdr *bytes.Reader
		if body != "" {
			rdr = bytes.NewReader([]byte(body))
		}
		var req *http.Request
		if rdr != nil {
			req = httptest.NewRequest(method, target, rdr)
			req.Header.Set("Content-Type", "application/json")
		} else {
			req = httptest.NewRequest(method, target, nil)
		}
		ctx := context.WithValue(req.Context(), EmailKey, email)
		req = req.WithContext(ctx)
		rec := httptest.NewRecorder()
		ServeZddc(cfg, rec, req)
		return rec
	}
	return cfg, do
}

func TestServeZddcAuthGate(t *testing.T) {
	// root admin = root@example.com; subtree admin alice@example.com on /projects.
	cfg, do := zddcTestSetup(t, map[string]string{
		"":           "admins:\n  - root@example.com\n",
		"projects":   "admins:\n  - alice@example.com\n",
		"projects/x": "",
	})

	cases := []struct {
		name       string
		method     string
		target     string
		email      string
		wantStatus int
	}{
		{"anon GET root", http.MethodGet, "/.profile/zddc?path=/", "", http.StatusNotFound},
		{"non-admin GET root", http.MethodGet, "/.profile/zddc?path=/", "mallory@example.com", http.StatusNotFound},
		{"super-admin GET root", http.MethodGet, "/.profile/zddc?path=/", "root@example.com", http.StatusOK},
		{"subtree-admin GET root (read-only)", http.MethodGet, "/.profile/zddc?path=/", "alice@example.com", http.StatusOK},
		{"subtree-admin GET own grant file (read-only)", http.MethodGet, "/.profile/zddc?path=/projects", "alice@example.com", http.StatusOK},
		{"subtree-admin GET deeper", http.MethodGet, "/.profile/zddc?path=/projects/x", "alice@example.com", http.StatusOK},
		{"subtree-admin POST own grant file (forbidden)", http.MethodPost, "/.profile/zddc?path=/projects", "alice@example.com", http.StatusForbidden},
		{"subtree-admin POST deeper (allowed)", http.MethodPost, "/.profile/zddc?path=/projects/x", "alice@example.com", http.StatusOK},
		{"super-admin POST root", http.MethodPost, "/.profile/zddc?path=/", "root@example.com", http.StatusOK},
		{"non-admin POST anywhere", http.MethodPost, "/.profile/zddc?path=/projects/x", "mallory@example.com", http.StatusNotFound},
		{"DELETE root rejected", http.MethodDelete, "/.profile/zddc?path=/", "root@example.com", http.StatusBadRequest},
		{"super-admin DELETE leaf", http.MethodDelete, "/.profile/zddc?path=/projects/x", "root@example.com", http.StatusNoContent},
	}

	_ = cfg
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			body := ""
			if tc.method == http.MethodPost {
				if tc.target == "/.profile/zddc?path=/" {
					// Root POST: writer must remain in admins list.
					body = `{"title":"","acl":{"allow":[],"deny":[]},"admins":["root@example.com"]}`
				} else {
					body = `{"title":"x","acl":{"allow":["*@example.com"],"deny":[]},"admins":[]}`
				}
			}
			rec := do(tc.method, tc.target, tc.email, body)
			if rec.Code != tc.wantStatus {
				t.Errorf("status = %d, want %d; body=%s", rec.Code, tc.wantStatus, rec.Body.String())
			}
		})
	}
}

func TestServeZddcGetReturnsChain(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":             "admins:\n  - root@example.com\nacl:\n  allow: [\"*@example.com\"]\n",
		"projects":     "title: All Projects\n",
		"projects/sub": "title: Substation\n",
	})
	rec := do(http.MethodGet, "/.profile/zddc?path=/projects/sub", "root@example.com", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
	}
	var resp zddcGetResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Path != "/projects/sub" {
		t.Errorf("path = %q, want /projects/sub", resp.Path)
	}
	if !resp.CanEdit {
		t.Errorf("CanEdit = false; root admin should edit anywhere")
	}
	if !resp.Exists {
		t.Errorf("Exists = false but file was written")
	}
	if len(resp.EffectiveChain) != 3 {
		t.Fatalf("chain length = %d, want 3 (root, projects, projects/sub)", len(resp.EffectiveChain))
	}
	if resp.EffectiveChain[0].Dir != "/" {
		t.Errorf("chain[0].Dir = %q, want /", resp.EffectiveChain[0].Dir)
	}
	if resp.EffectiveChain[1].Dir != "/projects" {
		t.Errorf("chain[1].Dir = %q, want /projects", resp.EffectiveChain[1].Dir)
	}
	if resp.EffectiveChain[2].Title != "Substation" {
		t.Errorf("chain[2].Title = %q, want Substation", resp.EffectiveChain[2].Title)
	}
}

func TestServeZddcPostValidatesGlob(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":         "admins:\n  - root@example.com\n",
		"projects": "",
	})
	body := `{"title":"x","acl":{"allow":["alice@@bad","good@example.com"],"deny":[]},"admins":[]}`
	rec := do(http.MethodPost, "/.profile/zddc?path=/projects", "root@example.com", body)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%s", rec.Code, rec.Body.String())
	}
	var we writeError
	if err := json.Unmarshal(rec.Body.Bytes(), &we); err != nil {
		t.Fatalf("decode err body: %v", err)
	}
	if len(we.Errors) == 0 || we.Errors[0].Field != "acl.allow[0]" {
		t.Errorf("expected acl.allow[0] error, got %+v", we.Errors)
	}
}

func TestServeZddcRootSelfDemotionRejected(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"": "admins:\n  - root@example.com\n  - bob@example.com\n",
	})
	// root tries to remove themselves, leaving only bob.
	body := `{"title":"","acl":{"allow":[],"deny":[]},"admins":["bob@example.com"]}`
	rec := do(http.MethodPost, "/.profile/zddc?path=/", "root@example.com", body)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400 (self-demotion rejected); body=%s", rec.Code, rec.Body.String())
	}
}

func TestServeZddcRootKeepingSelfAccepted(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"": "admins:\n  - root@example.com\n",
	})
	// root adds bob alongside themselves — fine.
	body := `{"title":"","acl":{"allow":[],"deny":[]},"admins":["root@example.com","bob@example.com"]}`
	rec := do(http.MethodPost, "/.profile/zddc?path=/", "root@example.com", body)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
}

func TestServeZddcWriteRoundTrip(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":         "admins:\n  - root@example.com\n",
		"projects": "",
	})
	body := `{"title":"Engineering","acl":{"allow":["*@varasys.io"],"deny":[]},"admins":["alice@varasys.io"]}`
	rec := do(http.MethodPost, "/.profile/zddc?path=/projects", "root@example.com", body)
	if rec.Code != http.StatusOK {
		t.Fatalf("write status = %d body=%s", rec.Code, rec.Body.String())
	}
	rec = do(http.MethodGet, "/.profile/zddc?path=/projects", "root@example.com", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("get status = %d body=%s", rec.Code, rec.Body.String())
	}
	var resp zddcGetResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.File.Title != "Engineering" {
		t.Errorf("title round-trip = %q, want Engineering", resp.File.Title)
	}
	if len(resp.File.Admins) != 1 || resp.File.Admins[0] != "alice@varasys.io" {
		t.Errorf("admins round-trip = %v, want [alice@varasys.io]", resp.File.Admins)
	}
}

func TestServeZddcWriteAppsRoundTrip(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":         "admins:\n  - root@example.com\n",
		"projects": "",
	})
	body := `{"title":"","acl":{"allow":[],"deny":[]},"admins":[],"apps":{` +
		`"default":"https://zddc.varasys.io/releases:stable",` +
		`"classifier":":beta",` +
		`"archive":"https://my.local.stuff/releases"}}`
	rec := do(http.MethodPost, "/.profile/zddc?path=/projects", "root@example.com", body)
	if rec.Code != http.StatusOK {
		t.Fatalf("write status=%d body=%s", rec.Code, rec.Body.String())
	}
	rec = do(http.MethodGet, "/.profile/zddc?path=/projects", "root@example.com", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("get status=%d body=%s", rec.Code, rec.Body.String())
	}
	var resp zddcGetResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if got := resp.File.Apps["default"]; got != "https://zddc.varasys.io/releases:stable" {
		t.Errorf("default round-trip = %q", got)
	}
	if got := resp.File.Apps["classifier"]; got != ":beta" {
		t.Errorf("classifier round-trip = %q", got)
	}
	if got := resp.File.Apps["archive"]; got != "https://my.local.stuff/releases" {
		t.Errorf("archive round-trip = %q", got)
	}
}

func TestServeZddcWriteAppsRejectsBadSpec(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":         "admins:\n  - root@example.com\n",
		"projects": "",
	})
	body := `{"title":"","acl":{"allow":[],"deny":[]},"admins":[],"apps":{"archive":"this is garbage"}}`
	rec := do(http.MethodPost, "/.profile/zddc?path=/projects", "root@example.com", body)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status=%d (want 400)", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), `"apps.archive"`) {
		t.Errorf("expected per-field error for apps.archive; got %s", rec.Body.String())
	}
}

func TestServeZddcEditorRendersAppsSection(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":         "admins:\n  - root@example.com\n",
		"projects": "apps:\n  default: \":beta\"\n  classifier: \"v0.0.4\"\n",
	})
	rec := do(http.MethodGet, "/.profile/zddc/edit?path=/projects", "root@example.com", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{
		"Apps (tool HTML sources)",
		`data-apps-key="default"`,
		`data-apps-key="archive"`,
		`data-apps-key="classifier"`,
		`data-apps-key="mdedit"`,
		`data-apps-key="transmittal"`,
		`data-apps-key="landing"`,
		`value=":beta"`,
		`value="v0.0.4"`,
		"classifier_v0.0.4.html", // preview reflects the cascaded resolution
	} {
		if !strings.Contains(body, want) {
			t.Errorf("editor body missing %q", want)
		}
	}
}

func TestServeZddcTreeFiltersByVisibility(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":        "admins:\n  - root@example.com\n",
		"alpha":   "admins:\n  - alice@example.com\n",
		"alpha/x": "title: alpha-x\n",
		"beta":    "admins:\n  - bob@example.com\n",
	})
	// alice sees alpha (her grant) and alpha/x (descendant), but not beta.
	rec := do(http.MethodGet, "/.profile/zddc/tree", "alice@example.com", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var entries []treeEntry
	if err := json.Unmarshal(rec.Body.Bytes(), &entries); err != nil {
		t.Fatalf("decode: %v", err)
	}
	seen := map[string]bool{}
	for _, e := range entries {
		seen[e.Path] = true
	}
	if !seen["/alpha"] || !seen["/alpha/x"] {
		t.Errorf("alice should see /alpha and /alpha/x; got %v", seen)
	}
	if seen["/beta"] {
		t.Errorf("alice should NOT see /beta; got %v", seen)
	}
}

func TestServeZddcEditorRenders(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":         "admins:\n  - root@example.com\n",
		"projects": "title: Engineering\n",
	})
	rec := do(http.MethodGet, "/.profile/zddc/edit?path=/projects", "root@example.com", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Engineering") {
		t.Errorf("editor should pre-fill title; body did not contain 'Engineering'")
	}
	if !strings.Contains(body, "/.profile/zddc?path=") {
		t.Errorf("editor should reference API URL; body lacks /.profile/zddc?path=")
	}
	if !strings.Contains(body, "Subtree admins of /projects") {
		t.Errorf("editor should label admins section as subtree (not bootstrap) for non-root file")
	}
}

func TestServeZddcEditorReadOnlyForNonEditor(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"":         "admins:\n  - root@example.com\n",
		"projects": "admins:\n  - alice@example.com\n",
	})
	// alice viewing her own grant file: read-only.
	rec := do(http.MethodGet, "/.profile/zddc/edit?path=/projects", "alice@example.com", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Read-only") {
		t.Errorf("editor should show Read-only banner for non-editor; body lacks it")
	}
}

func TestServeZddcRejectsReservedPathSegments(t *testing.T) {
	_, do := zddcTestSetup(t, map[string]string{
		"": "admins:\n  - root@example.com\n",
	})
	for _, p := range []string{"/.foo", "/_bar", "/projects/.evil"} {
		rec := do(http.MethodGet, "/.profile/zddc?path="+p, "root@example.com", "")
		if rec.Code != http.StatusNotFound {
			t.Errorf("path=%q expected 404, got %d", p, rec.Code)
		}
	}
}

func TestServeZddcAdminDispatchUnchangedForOtherRoutes(t *testing.T) {
	// Confirm that putting /.profile/zddc/* under the broader gate did not
	// regress the super-admin gate on /.profile/whoami etc.
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("admins:\n  - root@example.com\n"), 0o644); err != nil {
		t.Fatalf("write .zddc: %v", err)
	}
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/.profile/whoami", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
	rec := httptest.NewRecorder()
	ServeProfile(cfg, nil, nil, rec, req)
	if rec.Code != http.StatusNotFound {
		t.Errorf("non-admin /.profile/whoami got %d, want 404", rec.Code)
	}

	req = httptest.NewRequest(http.MethodGet, "/.profile/whoami", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "root@example.com"))
	rec = httptest.NewRecorder()
	ServeProfile(cfg, nil, nil, rec, req)
	if rec.Code != http.StatusOK {
		t.Errorf("super-admin /.profile/whoami got %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
}

func TestServeZddcAssetsCustomCSS(t *testing.T) {
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("admins:\n  - root@example.com\n"), 0o644); err != nil {
		t.Fatalf("write .zddc: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, ".admin.css"), []byte("body { color: red; }"), 0o644); err != nil {
		t.Fatalf("write .admin.css: %v", err)
	}
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/.profile/zddc/assets/custom.css", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "root@example.com"))
	rec := httptest.NewRecorder()
	ServeZddc(cfg, rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/css") {
		t.Errorf("Content-Type = %q, want text/css...", ct)
	}
	if !strings.Contains(rec.Body.String(), "color: red") {
		t.Errorf("body does not contain custom CSS")
	}
}

func TestServeZddcAssetsAbsentReturns404(t *testing.T) {
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("admins:\n  - root@example.com\n"), 0o644); err != nil {
		t.Fatalf("write .zddc: %v", err)
	}
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/.profile/zddc/assets/custom.css", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "root@example.com"))
	rec := httptest.NewRecorder()
	ServeZddc(cfg, rec, req)
	if rec.Code != http.StatusNotFound {
		t.Errorf("status=%d, want 404", rec.Code)
	}
}
