package handler

import (
	"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"
)

func TestIsZddcFileRequest(t *testing.T) {
	cases := []struct {
		url  string
		want bool
	}{
		{"/.zddc", true},
		{"/.zddc/", true},
		{"/Project/.zddc", true},
		{"/Project/archive/PartyA/mdl/.zddc", true},
		{"/.zddc.html", false}, // editor leaf, handled separately
		{"/Project/.zddc.html", false},
		{"/Project/.zddc/", true},
		{"/Project/", false},
		{"/", false},
	}
	for _, tc := range cases {
		if got := IsZddcFileRequest(tc.url); got != tc.want {
			t.Errorf("IsZddcFileRequest(%q) = %v, want %v", tc.url, got, tc.want)
		}
	}
}

func TestServeZddcFile_ExistingFile(t *testing.T) {
	root := t.TempDir()
	mustWrite(t, filepath.Join(root, ".zddc"),
		"title: root\nacl:\n  permissions:\n    \"*\": rwcda\n")
	subDir := filepath.Join(root, "Project")
	if err := os.Mkdir(subDir, 0o755); err != nil {
		t.Fatal(err)
	}
	mustWrite(t, filepath.Join(subDir, ".zddc"),
		"title: project-level\n")

	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/Project/.zddc", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
	rec := httptest.NewRecorder()
	ServeZddcFile(cfg, rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "title: project-level") {
		t.Errorf("body missing on-disk content: %q", rec.Body.String())
	}
	if got := rec.Header().Get("X-ZDDC-Source"); !strings.HasPrefix(got, "file:") {
		t.Errorf("X-ZDDC-Source = %q, want file:* prefix", got)
	}
	if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "application/yaml") {
		t.Errorf("Content-Type = %q, want application/yaml*", got)
	}
}

// TestServeZddcFile_VirtualDefault — when no .zddc is on disk at the
// requested directory, the body is the cascade's leaf-level ZddcFile
// marshalled as YAML, prefixed by a header comment explaining what
// the file is and pointing at ?effective=1 for the composed view.
//
// At /Project/.zddc with no on-disk file, the leaf is the embedded
// defaults' paths.* contribution — i.e. the project-scoped baseline
// (project_team: r, observer: r, document_controller: rw) plus the
// canonical paths: tree (archive, working, staging, reviewing, …).
// Asserts a few load-bearing markers; the full content is the
// `internal/zddc/defaults/` source-of-truth, which lives under
// zddc/internal/zddc and is parsed at every cascade walk.
func TestServeZddcFile_VirtualDefault(t *testing.T) {
	root := t.TempDir()
	// Directory exists but has no .zddc.
	subDir := filepath.Join(root, "Project")
	if err := os.Mkdir(subDir, 0o755); err != nil {
		t.Fatal(err)
	}
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/Project/.zddc", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
	rec := httptest.NewRecorder()
	ServeZddcFile(cfg, rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
	}
	if got := rec.Header().Get("X-ZDDC-Source"); got != "virtual:zddc" {
		t.Errorf("X-ZDDC-Source = %q, want virtual:zddc", got)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Virtual .zddc") {
		t.Errorf("body missing virtual header comment: %q", body)
	}
	if !strings.Contains(body, "?effective=1") {
		t.Errorf("body missing pointer to the composed-view query: %q", body)
	}
	// The embedded defaults declare project_team: r and
	// observer: r at paths.*. Confirm both surface so the user
	// sees the project-scoped baseline.
	if !strings.Contains(body, "project_team: r") {
		t.Errorf("body missing project_team grant from embedded defaults: %q", body)
	}
	if !strings.Contains(body, "observer: r") {
		t.Errorf("body missing observer grant from embedded defaults: %q", body)
	}
	// The paths: subtree below should include archive (the only
	// physical project-root child) and the virtual aggregators.
	if !strings.Contains(body, "archive:") {
		t.Errorf("body missing archive subtree: %q", body)
	}
}

// TestServeZddcFile_VirtualEmpty — at a directory the embedded
// defaults' paths: tree does NOT cover, the body collapses to the
// header comment + an empty-document placeholder ({}). The user
// sees "no rules declared at this exact level".
func TestServeZddcFile_VirtualEmpty(t *testing.T) {
	root := t.TempDir()
	// /Project/random-subfolder/ is not declared in the embedded
	// defaults' paths tree (paths.* matches the project name, but
	// no child path matches "random-subfolder").
	deep := filepath.Join(root, "Project", "random-subfolder")
	if err := os.MkdirAll(deep, 0o755); err != nil {
		t.Fatal(err)
	}
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/Project/random-subfolder/.zddc", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
	rec := httptest.NewRecorder()
	ServeZddcFile(cfg, rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Virtual .zddc") {
		t.Errorf("body missing virtual header: %q", body)
	}
	if !strings.Contains(body, "{}") {
		t.Errorf("undeclared-level body should end in {}: %q", body)
	}
	if !strings.Contains(body, "inherited from ancestors") {
		t.Errorf("undeclared-level body should explain inheritance: %q", body)
	}
}

// TestServeZddcFile_Effective_BasicCompose — ?effective=1 returns the
// merged composed view across embedded baseline + on-disk levels.
// Body is JSON with the merged ZddcFile and per-level source list.
func TestServeZddcFile_Effective_BasicCompose(t *testing.T) {
	root := t.TempDir()
	mustWrite(t, filepath.Join(root, ".zddc"),
		"acl:\n  permissions:\n    \"alice@example.com\": rwcda\n")
	proj := filepath.Join(root, "Project")
	if err := os.Mkdir(proj, 0o755); err != nil {
		t.Fatal(err)
	}
	mustWrite(t, filepath.Join(proj, ".zddc"),
		"title: My Project\n")
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/Project/.zddc?effective=1", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
	rec := httptest.NewRecorder()
	ServeZddcFile(cfg, rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
	}
	if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "application/json") {
		t.Errorf("Content-Type = %q, want application/json", got)
	}
	if got := rec.Header().Get("X-ZDDC-Source"); got != "virtual:effective" {
		t.Errorf("X-ZDDC-Source = %q, want virtual:effective", got)
	}

	var view effectiveZddcView
	if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
		t.Fatalf("decode: %v (body: %s)", err, rec.Body.String())
	}
	if view.URLPath != "/Project" {
		t.Errorf("url_path = %q, want /Project", view.URLPath)
	}
	// Merged should carry alice's grant (from root) AND the title
	// from /Project, AND the project_team grant from the embedded
	// defaults' paths.* contribution.
	if view.Merged.ACL.Permissions["alice@example.com"] != "rwcda" {
		t.Errorf("merged.acl.permissions missing alice's grant: %+v", view.Merged.ACL.Permissions)
	}
	if view.Merged.ACL.Permissions["project_team"] != "r" {
		t.Errorf("merged.acl.permissions missing project_team (from embedded defaults paths.*): %+v", view.Merged.ACL.Permissions)
	}
	if view.Merged.Title != "My Project" {
		t.Errorf("merged.title = %q, want My Project (from /Project/.zddc)", view.Merged.Title)
	}
	// Sources should include the embedded baseline (level -1) and
	// the two on-disk levels.
	var levels []int
	for _, s := range view.Sources {
		levels = append(levels, s.Level)
	}
	wantLevels := map[int]bool{-1: true, 0: true, 1: true}
	for _, l := range levels {
		delete(wantLevels, l)
	}
	if len(wantLevels) > 0 {
		t.Errorf("missing source levels %v in %v", wantLevels, levels)
	}
	// Per-level URLs are populated.
	for _, s := range view.Sources {
		if s.Level == -1 && s.URL != "<embedded>" {
			t.Errorf("embedded source url = %q, want <embedded>", s.URL)
		}
		if s.Level == 0 && s.URL != "/.zddc" {
			t.Errorf("root source url = %q, want /.zddc", s.URL)
		}
		if s.Level == 1 && s.URL != "/Project/.zddc" {
			t.Errorf("project source url = %q, want /Project/.zddc", s.URL)
		}
	}
}

// TestServeZddcFile_Effective_InheritFence — inherit:false at a level
// drops every ancestor (including the embedded baseline) from the
// composed view. Only the fence-and-below contribute.
func TestServeZddcFile_Effective_InheritFence(t *testing.T) {
	root := t.TempDir()
	mustWrite(t, filepath.Join(root, ".zddc"),
		"acl:\n  permissions:\n    \"alice@example.com\": rwcda\n")
	proj := filepath.Join(root, "Closed")
	if err := os.Mkdir(proj, 0o755); err != nil {
		t.Fatal(err)
	}
	// inherit:false on Closed/.zddc — root + embedded both drop
	// out of the visible chain.
	// Top-level inherit:false drops EVERY ancestor including the
	// embedded baseline. (ACL.inherit:false would only fence ACL
	// evaluation — roles, paths-tree, and embedded baseline still
	// cascade through, which is a separate test.)
	mustWrite(t, filepath.Join(proj, ".zddc"),
		"inherit: false\n"+
			"acl:\n  inherit: false\n  permissions:\n    \"bob@example.com\": rwcda\n")
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	// Bob has the only grant inside the fence; alice's root grant
	// is hidden by inherit:false so she'd 404 on the read gate.
	req := httptest.NewRequest(http.MethodGet, "/Closed/.zddc?effective=1", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "bob@example.com"))
	rec := httptest.NewRecorder()
	ServeZddcFile(cfg, rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
	}
	var view effectiveZddcView
	if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
		t.Fatalf("decode: %v", err)
	}
	// Alice's root grant must be invisible behind the fence.
	if _, ok := view.Merged.ACL.Permissions["alice@example.com"]; ok {
		t.Errorf("alice's root grant should be hidden by fence; got %+v", view.Merged.ACL.Permissions)
	}
	// Bob's grant at Closed/ is visible.
	if view.Merged.ACL.Permissions["bob@example.com"] != "rwcda" {
		t.Errorf("bob's fence-level grant missing: %+v", view.Merged.ACL.Permissions)
	}
	// Embedded baseline (level -1) must not appear in sources — the
	// fence zeroed it.
	for _, s := range view.Sources {
		if s.Level == -1 {
			t.Errorf("embedded baseline leaked past inherit:false fence: %+v", s)
		}
		if s.Level == 0 {
			t.Errorf("root /.zddc leaked past inherit:false fence: %+v", s)
		}
	}
}

// TestServeZddcFile_Effective_RoleMemberUnion — roles defined at
// multiple levels show the union of members (the runtime ACL
// evaluator uses lookupRoleMembers' union, and the composed view
// must match).
func TestServeZddcFile_Effective_RoleMemberUnion(t *testing.T) {
	root := t.TempDir()
	mustWrite(t, filepath.Join(root, ".zddc"),
		"acl:\n  permissions:\n    \"alice@example.com\": r\n"+
			"roles:\n  document_controller:\n    members:\n      - root-dc@example.com\n")
	proj := filepath.Join(root, "Project")
	if err := os.Mkdir(proj, 0o755); err != nil {
		t.Fatal(err)
	}
	mustWrite(t, filepath.Join(proj, ".zddc"),
		"roles:\n  document_controller:\n    members:\n      - project-dc@example.com\n")
	zddc.InvalidateCache(root)
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/Project/.zddc?effective=1", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
	rec := httptest.NewRecorder()
	ServeZddcFile(cfg, rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
	}
	var view effectiveZddcView
	if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
		t.Fatalf("decode: %v", err)
	}
	dc, ok := view.Merged.Roles["document_controller"]
	if !ok {
		t.Fatalf("merged.roles missing document_controller: %+v", view.Merged.Roles)
	}
	wantMembers := map[string]bool{
		"root-dc@example.com":    true,
		"project-dc@example.com": true,
	}
	for _, m := range dc.Members {
		delete(wantMembers, m)
	}
	if len(wantMembers) > 0 {
		t.Errorf("document_controller members missing %v; got %v", wantMembers, dc.Members)
	}
}

// TestServeZddcFile_VirtualWorkingPeer — the working/ peer declared by
// the embedded defaults shows its rich config in the synthesized virtual
// .zddc: default_tool, available_tools (classifier), party_source, history.
func TestServeZddcFile_VirtualWorkingPeer(t *testing.T) {
	root := t.TempDir()
	cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}

	req := httptest.NewRequest(http.MethodGet, "/Project/working/.zddc", nil)
	req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
	rec := httptest.NewRecorder()
	ServeZddcFile(cfg, rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{
		"default_tool: browse", // working/ default_tool
		"party_source: ssr",    // party gating
		"classifier",           // available_tools includes classifier
	} {
		if !strings.Contains(body, want) {
			t.Errorf("body missing %q at working/ peer: %s", want, body)
		}
	}
}
