package web

import (
	"regexp"
	"strings"
	"testing"
)

// v0.146.0 nav polish. The sidebar's collapsible groups are a PROGRESSIVE ENHANCEMENT: the server
// marks the group owning the active page with .is-open, so the correct group is already open before
// any JS runs and stays open if JS never runs at all. That server-side half is what these tests
// pin — the part a browser screenshot would confirm only for whichever page happened to be open.
//
// Red-proof: drop the `{{if $storageOpen}} is-open{{end}}` (or the matching aria-expanded) from
// layout.html and TestNavGroup_ActiveGroupRendersOpenServerSide fails.

// navGroupRe finds a sidebar group <li> and captures its class list and the id of the group it owns.
var navGroupRe = regexp.MustCompile(`<li class="(nav-group[^"]*)">\s*<button type="button" class="(nav-group-toggle[^"]*)" aria-expanded="(true|false)" aria-controls="(nav-group-[a-z]+)"`)

type navGroup struct {
	liClass, btnClass, ariaExpanded, id string
}

func parseNavGroups(t *testing.T, html string) map[string]navGroup {
	t.Helper()
	out := map[string]navGroup{}
	for _, m := range navGroupRe.FindAllStringSubmatch(html, -1) {
		out[m[4]] = navGroup{liClass: m[1], btnClass: m[2], ariaExpanded: m[3], id: m[4]}
	}
	if len(out) == 0 {
		t.Fatal("no sidebar nav groups parsed — the accordion markup or this pattern has drifted")
	}
	return out
}

// renderNavFor renders any page through the shared layout so the sidebar is exercised for real,
// rather than asserting against a hand-built fragment.
func renderNavFor(t *testing.T, page string) string {
	t.Helper()
	return renderBackupPage(t, "backups", map[string]interface{}{
		"Page": page, "Title": "teszt",
		"Backup":      nil,
		"GuestBackup": map[string]interface{}{"Available": false, "Note": "n/a"},
	})
}

func TestNavGroup_ActiveGroupRendersOpenServerSide(t *testing.T) {
	// Every sub-page, mapped to the group that must be open when it is the active page.
	cases := map[string]string{
		"storage":         "nav-group-storage",
		"storage-network": "nav-group-storage",
		"backups":         "nav-group-backups",
		"backups-remote":  "nav-group-backups",
		"backups-apps":    "nav-group-backups",
		"backups-restore": "nav-group-backups",
		"sharing":         "nav-group-sharing",
	}

	for page, wantOpen := range cases {
		groups := parseNavGroups(t, renderNavFor(t, page))

		openCount := 0
		for id, g := range groups {
			isOpen := strings.Contains(g.liClass, "is-open")
			if isOpen {
				openCount++
			}
			if id == wantOpen {
				if !isOpen {
					t.Errorf("page %q: group %s should render open, class was %q", page, id, g.liClass)
				}
				if g.ariaExpanded != "true" {
					t.Errorf("page %q: group %s open but aria-expanded=%q", page, id, g.ariaExpanded)
				}
				// The header must also read as active, or the open group looks unselected.
				if !strings.Contains(g.btnClass, "active") {
					t.Errorf("page %q: group %s open but its toggle is not .active (%q)", page, id, g.btnClass)
				}
			} else {
				if isOpen {
					t.Errorf("page %q: group %s should be closed, class was %q", page, id, g.liClass)
				}
				if g.ariaExpanded != "false" {
					t.Errorf("page %q: group %s closed but aria-expanded=%q", page, id, g.ariaExpanded)
				}
			}
		}
		// "Exactly one open" is the whole contract — assert the count, not just the one we expected.
		if openCount != 1 {
			t.Errorf("page %q: %d groups rendered open, want exactly 1", page, openCount)
		}
	}
}

func TestNavGroup_PageOutsideAnyGroupOpensNothing(t *testing.T) {
	// A top-level page (Vezérlőpult) belongs to no group; nothing should be forced open.
	groups := parseNavGroups(t, renderNavFor(t, "dashboard"))
	for id, g := range groups {
		if strings.Contains(g.liClass, "is-open") {
			t.Errorf("dashboard: group %s rendered open, want all closed", id)
		}
		if g.ariaExpanded != "false" {
			t.Errorf("dashboard: group %s aria-expanded=%q, want false", id, g.ariaExpanded)
		}
	}
}

func TestNavGroup_EveryGroupLandingPageSurvivesAsASubItem(t *testing.T) {
	// The group headers stopped being links in v0.146.0, which is only safe because each group's own
	// landing page is ALSO its first sub-item. If someone removes one of those sub-links, that
	// destination becomes unreachable from the sidebar — silently, since the header still looks
	// clickable. This asserts the property the conversion depended on.
	html := renderNavFor(t, "dashboard")
	for _, href := range []string{"/storage", "/backups", "/sharing"} {
		if !strings.Contains(html, `href="`+href+`"`) {
			t.Errorf("no sidebar link to %q — a group landing page is unreachable now that its header is a <button>", href)
		}
	}
}

func TestNavGroup_HeaderIsARealButtonWithAControlsTarget(t *testing.T) {
	html := renderNavFor(t, "storage")

	// Keyboard/AT reachability rides on it being a real button, not a div with a click handler.
	if strings.Contains(html, `<div class="nav-group-toggle`) {
		t.Error("nav-group-toggle rendered as a div — keyboard and assistive-tech reachability would be simulated, not real")
	}
	for id, g := range parseNavGroups(t, html) {
		if !strings.HasPrefix(g.btnClass, "nav-group-toggle") {
			t.Errorf("group %s: toggle class %q", id, g.btnClass)
		}
		// aria-controls must point at a list that actually exists in the document.
		if !strings.Contains(html, `id="`+g.id+`"`) {
			t.Errorf("group %s: aria-controls=%q targets no element", id, g.id)
		}
	}
}
