package web

import (
	"bytes"
	"strings"
	"testing"
)

// v0.166.0 mobile navigation rework. The old @media(max-width:768px) block predated the v0.146.0
// accordion: it flattened .nav-links into a horizontal, overflow-x strip, which clipped the
// accordion's nested sub-lists (they share the .nav-links class). This suite pins the replacement:
// a hamburger + off-canvas drawer that REUSES the existing vertical sidebar, plus a no-JS static
// fallback so navigation never dead-ends. Desktop (>768px) is untouched — the accordion invariants
// stay in nav_accordion_test.go and must pass unchanged.

// mediaBlock768 returns the full brace-balanced body of the @media(max-width: 768px) rule so CSS
// pins are scoped to the mobile block rather than matching an unrelated rule elsewhere in the sheet.
func mediaBlock768(t *testing.T, css string) string {
	t.Helper()
	marker := "@media(max-width: 768px)"
	i := strings.Index(css, marker)
	if i < 0 {
		t.Fatalf("no %q media block found — the mobile breakpoint moved or was renamed", marker)
	}
	open := strings.IndexByte(css[i:], '{')
	if open < 0 {
		t.Fatal("no opening brace after the 768px media marker")
	}
	start := i + open
	depth := 0
	for j := start; j < len(css); j++ {
		switch css[j] {
		case '{':
			depth++
		case '}':
			depth--
			if depth == 0 {
				return css[start : j+1]
			}
		}
	}
	t.Fatal("unbalanced braces in the 768px media block")
	return ""
}

// Group B — CSS pins (Scenario A/B/C).
//
// Red-proof: run against the pre-edit style.css and the strip-absence assertions FAIL — the old
// block still carries `.nav-links { display:flex; ... overflow-x:auto }`.
func TestMobileCSS_StripRemovedDrawerPresent(t *testing.T) {
	data, err := StyleCSS()
	if err != nil {
		t.Fatalf("StyleCSS(): %v", err)
	}
	css := string(data)
	block := mediaBlock768(t, css)

	// The horizontal strip that clipped the accordion is gone from the mobile block.
	if strings.Contains(block, "overflow-x") {
		t.Error("768px block still contains an overflow-x rule — the horizontal .nav-links strip was not removed")
	}
	if strings.Contains(block, ".nav-links {") || strings.Contains(block, ".nav-links{") {
		t.Error("768px block still flattens .nav-links — the strip pattern must be deleted, not patched")
	}

	// The off-canvas drawer + no-JS fallback are present in the mobile block.
	for _, want := range []string{
		".js .sidebar",      // JS path: sidebar becomes an off-canvas drawer
		"translateX(-100%)", // parked off-canvas until opened
		".no-js .sidebar",   // no-JS path: sidebar renders static inline
		"body.nav-open",     // scroll lock while the drawer is open
		".mobile-topbar",    // the sticky top bar is styled inside the mobile block
	} {
		if !strings.Contains(block, want) {
			t.Errorf("768px block missing %q — the drawer/fallback rules are incomplete", want)
		}
	}

	// The desktop defaults that keep >768px pixel-identical live OUTSIDE the media query.
	for _, want := range []string{".mobile-topbar", ".nav-backdrop", ".nav-burger"} {
		if !strings.Contains(css, want) {
			t.Errorf("stylesheet missing base rule for %q", want)
		}
	}
	if !strings.Contains(css, ".mobile-topbar { display: none") &&
		!strings.Contains(css, ".mobile-topbar{display:none") {
		t.Error("no desktop-default `.mobile-topbar { display:none }` — the top bar would leak onto desktop")
	}
}

// The sidebar logo is horizontally centered in its header (desktop sidebar + mobile drawer share
// the element). Red-proof: revert to `margin-bottom: 0.25rem` (no auto side-margins) → this FAILS.
func TestSidebarLogo_Centered(t *testing.T) {
	data, err := StyleCSS()
	if err != nil {
		t.Fatalf("StyleCSS(): %v", err)
	}
	css := string(data)
	i := strings.Index(css, ".sidebar-logo {")
	if i < 0 {
		t.Fatal(".sidebar-logo rule not found")
	}
	rule := css[i : i+strings.Index(css[i:], "}")]
	if !strings.Contains(rule, "margin: 0 auto") {
		t.Errorf(".sidebar-logo is not centered (no `margin: 0 auto`); rule was:\n%s", rule)
	}
}

// Group A — drawer markup (Scenario A/C), asserted through the real layout render.
//
// Red-proof: remove aria-controls from the burger button in layout.html → the aria-controls
// assertion FAILS. (Recorded in REPORT.)
func TestMobileNav_TopbarAndDrawerMarkup(t *testing.T) {
	html := renderNavFor(t, "dashboard")

	// Progressive-enhancement hook: the html element starts as no-js and the head script swaps it.
	if !strings.Contains(html, `<html lang="hu" class="no-js">`) {
		t.Error("<html> element does not carry class=\"no-js\" — the no-JS CSS fallback would never engage")
	}
	if !strings.Contains(html, `replace('no-js','js')`) {
		t.Error("head script does not swap no-js → js")
	}

	// Sticky top bar with a single burger toggle wired to the sidebar.
	if !strings.Contains(html, `class="mobile-topbar"`) {
		t.Error("no .mobile-topbar element rendered")
	}
	if !strings.Contains(html, `class="nav-burger"`) {
		t.Error("no .nav-burger button rendered")
	}
	if !strings.Contains(html, `aria-expanded="false"`) {
		t.Error("burger missing aria-expanded=\"false\" initial state")
	}
	if !strings.Contains(html, `aria-controls="sidebar"`) {
		t.Error("burger missing aria-controls=\"sidebar\" — the button is not associated with the drawer")
	}

	// The sidebar is the drawer target, and the backdrop starts hidden.
	if !strings.Contains(html, `id="sidebar"`) {
		t.Error("sidebar has no id=\"sidebar\" — aria-controls points at nothing")
	}
	if !strings.Contains(html, `class="nav-backdrop"`) {
		t.Error("no .nav-backdrop element rendered")
	}
	// The backdrop must default hidden (both the attribute and its element must be present together).
	if !strings.Contains(html, `class="nav-backdrop" hidden`) {
		t.Error("nav-backdrop is not initially hidden")
	}
}

// Group D — customer name (Scenario D).
//
// Red-proof: re-add <span class="customer-name">{{.CustomerName}}</span> to the sidebar header →
// TestSidebar_NoCustomerName FAILS.
func TestSidebar_NoCustomerName(t *testing.T) {
	html := renderNavFor(t, "dashboard")
	if strings.Contains(html, "customer-name") {
		t.Error("sidebar still renders the customer-name element — it must live only on the login page")
	}
}

func TestLogin_CustomerNameKept(t *testing.T) {
	s := testServer(t)
	s.loadTemplates()
	var buf bytes.Buffer
	data := map[string]interface{}{
		"Title":        "Bejelentkezés",
		"CustomerName": "Teszt Ügyfél",
		"Version":      "9.9.9",
	}
	if err := s.tmpl.ExecuteTemplate(&buf, "login", data); err != nil {
		t.Fatalf("render login: %v", err)
	}
	out := buf.String()
	// The login page still identifies whose box this is.
	if !strings.Contains(out, "Teszt Ügyfél") {
		t.Error("login page dropped the CustomerName subtitle — it identifies the box owner and must stay")
	}
	if !strings.Contains(out, `class="login-subtitle"`) {
		t.Error("login-subtitle element missing")
	}
}

// Group E — logo constants carry NO live text (Scenario E). Under `<img>` secure static mode only
// locally-installed fonts resolve, so a `font-family` in the SVG renders a fallback font everywhere;
// the wordmark must be outlined paths. Shipped in v0.167.0 once the outlined master landed on
// felhom.eu main and the vestigial empty text nodes were stripped.
//
// Red-proof (recorded 2026-07-24): run against the pre-v0.167.0 constants → FAILS, both constants
// contained `<text` + `font-family`.
func TestLogoSVG_NoLiveText(t *testing.T) {
	for name, s := range map[string]string{"FelhomLogoSVG": FelhomLogoSVG, "FelhomFaviconSVG": FelhomFaviconSVG} {
		if strings.Contains(s, "<text") {
			t.Errorf("%s contains a <text element — the wordmark must be outlined paths, not live text", name)
		}
		if strings.Contains(s, "font-family") {
			t.Errorf("%s contains font-family — it renders in a fallback font under <img> secure static mode", name)
		}
	}
}

// Group E — versioned asset URLs (Scenario E).
//
// Red-proof: run against the pre-edit layout.html → both assertions FAIL (the /static/ URLs lack
// the ?v= cache-bust). Cloudflare edge-caches /static/* for 4h, so an unversioned logo/favicon keeps
// serving the previous release after a deploy (same failure mode as the 0.126.1 CSS incident).
func TestLayout_VersionedAssetURLs(t *testing.T) {
	html := renderNavFor(t, "dashboard")
	if !strings.Contains(html, "/static/felhom-logo.svg?v=") {
		t.Error("sidebar logo URL is not cache-busted with ?v= — a deploy would keep serving the stale edge copy")
	}
	if !strings.Contains(html, "/static/favicon.svg?v=") {
		t.Error("favicon URL is not cache-busted with ?v=")
	}
}
