package access

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

// spec is a small helper to build a GroupSpec.
func spec(name, parent string, members map[string]Role) GroupSpec {
	return GroupSpec{Name: name, Parent: parent, Members: members}
}

func TestUnconfiguredIsOpen(t *testing.T) {
	// No roles assigned anywhere → fully open (everyone is admin), matching a
	// fresh/dev deployment.
	p := New("", nil)
	for _, u := range []string{"", "alice", "carol"} {
		if !p.CanAdmin(u) {
			t.Errorf("unconfigured: CanAdmin(%q) = false, want true", u)
		}
		if p.Level(u) != LevelWrite {
			t.Errorf("unconfigured: Level(%q) = %v, want write", u, p.Level(u))
		}
	}
}

func TestRoleLadder(t *testing.T) {
	p := New("", []GroupSpec{
		spec("public", "", map[string]Role{"adm": RoleAdmin}),
		spec("ops", "", map[string]Role{
			"viewer": RoleViewer,
			"op":     RoleOperator,
			"logic":  RoleLogic,
			"aud":    RoleAuditor,
		}),
	})

	// Configured now → unlisted users (and anonymous) are viewers (read-only).
	if p.Level("") != LevelRead {
		t.Errorf("anonymous Level = %v, want read", p.Level(""))
	}
	if p.Level("nobody") != LevelRead {
		t.Errorf("unlisted Level = %v, want read", p.Level("nobody"))
	}

	cases := []struct {
		user      string
		level     Level
		editLogic bool
		audit     bool
		admin     bool
	}{
		{"viewer", LevelRead, false, false, false},
		{"op", LevelWrite, false, false, false},
		{"logic", LevelWrite, true, false, false},
		{"aud", LevelWrite, true, true, false},
		{"adm", LevelWrite, true, true, true},
	}
	for _, c := range cases {
		if p.Level(c.user) != c.level {
			t.Errorf("%s: Level = %v, want %v", c.user, p.Level(c.user), c.level)
		}
		if p.CanEditLogic(c.user) != c.editLogic {
			t.Errorf("%s: CanEditLogic = %v, want %v", c.user, p.CanEditLogic(c.user), c.editLogic)
		}
		if p.CanViewAudit(c.user) != c.audit {
			t.Errorf("%s: CanViewAudit = %v, want %v", c.user, p.CanViewAudit(c.user), c.audit)
		}
		if p.CanAdmin(c.user) != c.admin {
			t.Errorf("%s: CanAdmin = %v, want %v", c.user, p.CanAdmin(c.user), c.admin)
		}
	}
}

func TestEffectiveRoleIsMaxAcrossGroups(t *testing.T) {
	p := New("", []GroupSpec{
		spec("public", "", map[string]Role{"x": RoleViewer}),
		spec("a", "", map[string]Role{"x": RoleOperator}),
		spec("b", "", map[string]Role{"x": RoleAuditor}),
	})
	if got := p.EffectiveRole("x"); got != RoleAuditor {
		t.Errorf("EffectiveRole(x) = %v, want auditor (max across groups)", got)
	}
}

func TestNestingInheritsMembership(t *testing.T) {
	// org → team-a → squad-1. A member of org is effectively in the descendants
	// too (membership flows parent→child), surfaced via GroupsOf for panel ACLs.
	p := New("", []GroupSpec{
		spec("org", "", map[string]Role{"boss": RoleAdmin}),
		spec("team-a", "org", nil),
		spec("squad-1", "team-a", nil),
	})
	groups := p.GroupsOf("boss")
	for _, want := range []string{"org", "team-a", "squad-1"} {
		if !containsStr(groups, want) {
			t.Errorf("GroupsOf(boss) = %v, missing %q", groups, want)
		}
	}
}

func TestMutationsRoundTrip(t *testing.T) {
	p := New("", []GroupSpec{
		spec("public", "", map[string]Role{"root": RoleAdmin}),
		spec("ops", "", map[string]Role{"carol": RoleOperator}),
	})

	// Set a user's full membership set: dave operator in ops, admin in eng (new).
	if err := p.SetUserRoles("dave", map[string]Role{"ops": RoleOperator, "eng": RoleAdmin}); err != nil {
		t.Fatal(err)
	}
	if g := p.GroupsOf("dave"); !containsStr(g, "ops") || !containsStr(g, "eng") {
		t.Errorf("GroupsOf(dave) = %v, want ops+eng", g)
	}
	if !p.CanAdmin("dave") { // admin in eng
		t.Error("CanAdmin(dave) = false after admin role in eng")
	}

	// Replacing membership removes dave from ops.
	if err := p.SetUserRoles("dave", map[string]Role{"eng": RoleAdmin}); err != nil {
		t.Fatal(err)
	}
	if containsStr(p.GroupsOf("dave"), "ops") {
		t.Error("dave still in ops after membership replaced")
	}

	// Rename carries dave's admin grant from eng → engineering.
	if err := p.RenameGroup("eng", "engineering"); err != nil {
		t.Fatal(err)
	}
	if !p.CanAdmin("dave") {
		t.Error("CanAdmin(dave) = false after eng renamed to engineering")
	}
	if !containsStr(p.GroupNames(), "engineering") || containsStr(p.GroupNames(), "eng") {
		t.Errorf("GroupNames after rename = %v", p.GroupNames())
	}

	// Delete drops the group; dave loses admin but root (in public) keeps it.
	if err := p.DeleteGroup("engineering"); err != nil {
		t.Fatal(err)
	}
	if p.CanAdmin("dave") {
		t.Error("CanAdmin(dave) = true after engineering deleted")
	}
	if !p.CanAdmin("root") {
		t.Error("CanAdmin(root) = false; root grant in public should survive")
	}
	if err := p.DeleteGroup("nope"); err != ErrNotFound {
		t.Errorf("DeleteGroup(nope) = %v, want ErrNotFound", err)
	}
}

func TestPublicGroupProtected(t *testing.T) {
	p := New("", []GroupSpec{spec("public", "", map[string]Role{"a": RoleAdmin})})
	if err := p.DeleteGroup(PublicGroup); err == nil {
		t.Error("DeleteGroup(public) = nil, want error")
	}
	if err := p.RenameGroup(PublicGroup, "other"); err == nil {
		t.Error("RenameGroup(public) = nil, want error")
	}
	if !containsStr(p.GroupNames(), PublicGroup) {
		t.Error("public group missing after protected mutations")
	}
}

func TestParentCycleRejected(t *testing.T) {
	p := New("", []GroupSpec{
		spec("a", "", map[string]Role{"x": RoleViewer}),
		spec("b", "a", nil),
	})
	// Making a's parent b would create a cycle a→b→a; it must be dropped to root.
	if err := p.SetGroup("a", "b", map[string]Role{"x": RoleViewer}); err != nil {
		t.Fatal(err)
	}
	snap := p.Snapshot()
	for _, g := range snap.Groups {
		if g.Name == "a" && g.Parent != "" {
			t.Errorf("group a parent = %q, want root (cycle should be dropped)", g.Parent)
		}
	}
}

func TestPersistenceRoundTrip(t *testing.T) {
	dir := t.TempDir()

	p := New("admin", []GroupSpec{
		spec("public", "", map[string]Role{"alice": RoleLogic}),
		spec("ops", "", map[string]Role{"carol": RoleAdmin}),
	})
	if err := p.EnablePersistence(dir); err != nil {
		t.Fatal(err)
	}
	// A mutation triggers the first write of access.json.
	if err := p.SetUserRoles("dave", map[string]Role{"ops": RoleOperator}); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(dir, "access.json")); err != nil {
		t.Fatalf("access.json not written: %v", err)
	}

	// A fresh policy loading the same dir should see the persisted state, not the
	// (different) seed it was constructed with.
	p2 := New("seed", []GroupSpec{spec("public", "", map[string]Role{"seeduser": RoleViewer})})
	if err := p2.EnablePersistence(dir); err != nil {
		t.Fatal(err)
	}
	if p2.ResolveUser("") != "admin" {
		t.Errorf("default user = %q, want admin", p2.ResolveUser(""))
	}
	if !p2.CanEditLogic("alice") || p2.CanEditLogic("zed") {
		t.Error("logic-editor role not persisted")
	}
	if !p2.CanAdmin("carol") {
		t.Error("admin role not persisted")
	}
	if !containsStr(p2.GroupsOf("dave"), "ops") {
		t.Error("dave membership not persisted")
	}
	if p2.EffectiveRole("seeduser") != RoleViewer || p2.Level("seeduser") != LevelRead {
		// seeduser came from the discarded seed; should be an unlisted viewer now.
		t.Error("persisted state did not supersede the seed")
	}
}

func TestConcurrentAccess(t *testing.T) {
	p := New("", []GroupSpec{
		spec("public", "", map[string]Role{"root": RoleAdmin}),
		spec("ops", "", map[string]Role{"carol": RoleOperator}),
	})
	if err := p.EnablePersistence(t.TempDir()); err != nil {
		t.Fatal(err)
	}
	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(2)
		go func() {
			defer wg.Done()
			for j := 0; j < 100; j++ {
				p.CanAdmin("carol")
				p.Level("carol")
				p.GroupsOf("carol")
				p.Snapshot()
			}
		}()
		go func() {
			defer wg.Done()
			for j := 0; j < 100; j++ {
				p.SetMemberRole("ops", "carol", RoleAuditor)
				p.SetUserRoles("carol", map[string]Role{"ops": RoleOperator, "eng": RoleAdmin})
				p.RemoveMember("ops", "carol")
			}
		}()
	}
	wg.Wait()
}

func containsStr(xs []string, v string) bool {
	for _, x := range xs {
		if x == v {
			return true
		}
	}
	return false
}
