package web

import (
	"encoding/json"
	"net/http/httptest"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)

// R-66 Leg A — the „Hálózat" card on Beállítások → Rendszer.

// A1: sharing enabled → the card renders address + network-name + gateway rows with live values.
func TestNetworkCardRendersAllRows(t *testing.T) {
	s := testPageServer(t)
	if err := s.settings.SetSMBEnabled(true); err != nil {
		t.Fatal(err)
	}
	s.sambaAddrFn = func() string { return "192.168.0.104" }
	s.guestGatewayFn = func() string { return "192.168.0.1" }

	body := getPage(t, s, "/settings").Body.String()
	for _, m := range []string{"Hálózat", "Helyi cím (LAN)", "192.168.0.104", "Hálózati név", `\\FELHOM`, "Átjáró", "192.168.0.1"} {
		if !strings.Contains(body, m) {
			t.Errorf("card missing %q", m)
		}
	}
	// The footer sentence (remote-troubleshooting read-aloud hint) belongs to the card.
	if !strings.Contains(body, "helyi hálózattól függenek") {
		t.Error("card missing the footer sentence")
	}
}

// A2: sharing DISABLED → the Hálózati név row is ABSENT. The WRONG case this pins: rendering
// \\FELHOM while samba is down would promise a name that does not exist on the network.
// Red-proof: drop the smb.Enabled gate in systemPageData → this fails with \\FELHOM present.
func TestNetworkCardNoSMBNameWhenSharingOff(t *testing.T) {
	s := testPageServer(t)
	if err := s.settings.SetSMBEnabled(false); err != nil {
		t.Fatal(err)
	}
	s.sambaAddrFn = func() string { return "" }
	s.guestGatewayFn = func() string { return "" }

	body := getPage(t, s, "/settings").Body.String()
	if !strings.Contains(body, "Hálózat") {
		t.Fatal("card absent entirely")
	}
	if strings.Contains(body, "Hálózati név") || strings.Contains(body, `\\FELHOM`) {
		t.Error("Hálózati név row rendered while Megosztás is disabled — a wrong promise")
	}
}

// A3: the address helper returns "" → the row renders „—" + the muted note, no crash, and no
// fallback value appears from anywhere (nothing is stored to fall back TO — S-5).
func TestNetworkCardDashOnUnavailable(t *testing.T) {
	s := testPageServer(t)
	s.sambaAddrFn = func() string { return "" }
	s.guestGatewayFn = func() string { return "" }

	rec := getPage(t, s, "/settings")
	if rec.Code != 200 {
		t.Fatalf("GET /settings = %d, want 200", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "nem állapítható meg") {
		t.Error("missing the muted unavailable note")
	}
	// No plausible-but-stale address may surface: the only IPs on the page must be the ones other
	// cards legitimately carry — assert the card region itself carries the dash.
	cardStart := strings.Index(body, "Helyi cím (LAN)")
	if cardStart < 0 {
		t.Fatal("card row missing")
	}
	region := body[cardStart:]
	if end := strings.Index(region, "Átjáró"); end > 0 {
		region = region[:end]
	}
	if !strings.Contains(region, "—") {
		t.Error("Helyi cím row lacks the em-dash placeholder")
	}
}

// R-66 Leg A seam freshness: the card must re-derive per render (never memoize a DHCP lease) —
// the same counted-fn assertion that guards the Megosztás connect card.
func TestNetworkCardFreshPerRender(t *testing.T) {
	s := testPageServer(t)
	addrCalls, gwCalls := 0, 0
	s.sambaAddrFn = func() string { addrCalls++; return "192.168.0.104" }
	s.guestGatewayFn = func() string { gwCalls++; return "192.168.0.1" }

	getPage(t, s, "/settings")
	getPage(t, s, "/settings")
	if addrCalls != 2 || gwCalls != 2 {
		t.Errorf("stale-value risk: addr resolved %d×, gateway %d× over 2 renders (want 2/2)", addrCalls, gwCalls)
	}
}

// R-66 Leg B (web half) — the Debug dump's network section.
// B1: the dump contains `network` with the four sub-keys; a fabricated resolv.conf failure yields
// an error string IN PLACE while the dump stays complete.
func TestDebugDumpNetworkSection(t *testing.T) {
	s := testPageServer(t)
	s.guestNetFn = func() stacks.GuestNetSnapshot {
		return stacks.GuestNetSnapshot{
			Interfaces:     []stacks.GuestInterface{{Name: "eth0", Up: true, Addresses: []string{"192.168.0.104/24"}}},
			Gateway:        "192.168.0.1",
			RouteInterface: "eth0",
			LANAddress:     "192.168.0.104",
			Errors:         map[string]string{"dns": "scripted resolv.conf failure"},
		}
	}

	rec := httptest.NewRecorder()
	s.debugDump(rec, httptest.NewRequest("GET", "/api/debug/dump", nil))
	if rec.Code != 200 {
		t.Fatalf("dump = %d, want 200", rec.Code)
	}
	var dump map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &dump); err != nil {
		t.Fatalf("dump not JSON: %v", err)
	}
	network, ok := dump["network"].(map[string]interface{})
	if !ok {
		t.Fatalf("dump lacks a network object: %T", dump["network"])
	}
	for _, key := range []string{"interfaces", "default_route", "dns_servers", "lan_address"} {
		if _, present := network[key]; !present {
			t.Errorf("network section missing %q", key)
		}
	}
	// The failed item reports in place…
	if e, _ := network["dns_servers"].(string); !strings.Contains(e, "scripted resolv.conf failure") {
		t.Errorf("dns_servers = %v, want the in-place error string", network["dns_servers"])
	}
	// …and the healthy items are real values, not casualties.
	if la, _ := network["lan_address"].(string); la != "192.168.0.104" {
		t.Errorf("lan_address = %v (must equal the Hálózat card's value)", network["lan_address"])
	}
	route, _ := network["default_route"].(map[string]interface{})
	if route["gateway"] != "192.168.0.1" || route["interface"] != "eth0" {
		t.Errorf("default_route = %v", network["default_route"])
	}
	// The dump as a whole stayed complete (existing sections intact).
	for _, key := range []string{"controller", "storage", "stacks"} {
		if _, present := dump[key]; !present {
			t.Errorf("dump lost its %q section", key)
		}
	}
}

// R-66 Leg C — the NetBIOS trap named on an unreachable-class add failure.
func TestNetAddMessageNetBIOSHint(t *testing.T) {
	const hintMark = "Windows-hálózati névnek tűnik"

	// C1: single-label non-IP name + unreachable → hint present, naming the submitted value.
	msg := netAddMessage("unreachable", "FELHOM", 1000)
	if !strings.Contains(msg, hintMark) || !strings.Contains(msg, "»FELHOM«") {
		t.Errorf("C1: hint missing from %q", msg)
	}

	// C2: an IP + unreachable → hint ABSENT (the wrong case: nagging an IP user about NetBIOS).
	// Red-proof: invert the lexical check in looksLikeFlatNetworkName → this fails.
	if msg := netAddMessage("unreachable", "192.168.0.50", 1000); strings.Contains(msg, hintMark) {
		t.Errorf("C2: hint wrongly present for an IP: %q", msg)
	}

	// C3: dotted name → hint absent.
	if msg := netAddMessage("unreachable", "nas.local", 1000); strings.Contains(msg, hintMark) {
		t.Errorf("C3: hint wrongly present for a dotted name: %q", msg)
	}

	// The hint stays out of every OTHER category — it explains unreachability only.
	if msg := netAddMessage("smb_auth", "FELHOM", 1000); strings.Contains(msg, hintMark) {
		t.Errorf("hint leaked into smb_auth: %q", msg)
	}

	// Lexical edges: IPv6 literal (no dots, but an IP) and empty stay quiet.
	if looksLikeFlatNetworkName("fe80::1") {
		t.Error("IPv6 literal flagged as a NetBIOS name")
	}
	if looksLikeFlatNetworkName("") || looksLikeFlatNetworkName("   ") {
		t.Error("empty value flagged as a NetBIOS name")
	}
	if !looksLikeFlatNetworkName("FELHOM") {
		t.Error("FELHOM not flagged")
	}
}
