package app

import (
	"strings"
	"testing"

	"kjol/vdom"
)

// The two-runtime demo.s whole claim is that its two panes are ONE function: the live
// component on the left, and the HTML string the server sends on the right. If they could
// drift, the page would be a lie told in the most embarrassing possible place.
//
// It lives on /wasm now, not on the front page — it is the Wasm Web engine.s argument,
// and the front page is kjøl.s. The test followed it.
//
// So: render it, click the button the way the browser would, render again, and check
// that BOTH panes moved. A pane rendered from a stale copy of the tree — or from a
// second, hand-written one — fails here.
func TestTwoRuntimePanesShareOneTree(t *testing.T) {
	page := DocsPage(Deps{Path: func() string { return "/wasm" }})

	html := vdom.RenderHTML(page())
	if !strings.Contains(html, "clicked 0 times") {
		t.Fatalf("the live pane did not render its initial state:\n%s", html)
	}
	// The right-hand pane is the ESCAPED HTML of the same tree, so the markup it shows
	// appears in the page's own markup double-escaped: &lt;div ...
	if !strings.Contains(html, "&lt;div class=") {
		t.Fatal("the right-hand pane is not showing rendered HTML at all")
	}

	clickButton(t, page(), "Click me")

	html = vdom.RenderHTML(page())
	if strings.Count(html, "clicked 1 times") < 2 {
		t.Errorf("after one click, %d panes say \"clicked 1 times\" — both should:\n%s",
			strings.Count(html, "clicked 1 times"), html)
	}
}

// The byte count under the right-hand pane is the length of the string actually shown,
// not a number typed in by hand — so it has to move when the markup does.
func TestTwoRuntimeByteCountIsReal(t *testing.T) {
	page := DocsPage(Deps{Path: func() string { return "/wasm" }})

	before := byteCountLabel(t, vdom.RenderHTML(page()))
	clickButton(t, page(), "Click me")
	// "clicked 0 times" -> "clicked 1 times" is the same length, so click into double
	// digits, where the markup genuinely grows by one byte.
	for i := 0; i < 10; i++ {
		clickButton(t, page(), "Click me")
	}
	after := byteCountLabel(t, vdom.RenderHTML(page()))

	if before == after {
		t.Errorf("the markup grew by a digit but the byte count did not move (%s) — it is not measuring the string", before)
	}
}

// byteCountLabel pulls the "N bytes of HTML" caption out of the rendered page.
func byteCountLabel(t *testing.T, html string) string {
	t.Helper()
	i := strings.Index(html, " bytes of HTML")
	if i < 0 {
		t.Fatal("no byte-count caption on the /wasm overview")
	}
	start := strings.LastIndexByte(html[:i], '>') + 1
	return html[start : i+len(" bytes of HTML")]
}

// clickButton finds a button by its label and fires its click handler.
func clickButton(t *testing.T, n *vdom.VNode, label string) {
	t.Helper()
	if !findAndClickButton(n, label) {
		t.Fatalf("no clickable button labelled %q on the page", label)
	}
}

func findAndClickButton(n *vdom.VNode, label string) bool {
	if n == nil {
		return false
	}
	if n.Tag == "button" && strings.Contains(textOf(n), label) {
		if h := n.Events[vdom.EVENT_CLICK]; h != nil {
			h(clickEvent{})
			return true
		}
	}
	for _, c := range n.Children {
		if findAndClickButton(c, label) {
			return true
		}
	}
	return false
}

func textOf(n *vdom.VNode) string {
	if n.Tag == "" {
		return n.Text
	}
	var b strings.Builder
	for _, c := range n.Children {
		b.WriteString(textOf(c))
	}
	return b.String()
}

// clickEvent is a vdom.Event with no DOM behind it — enough to invoke a handler.
type clickEvent struct{}

func (clickEvent) PreventDefault()       {}
func (clickEvent) StopPropagation()      {}
func (clickEvent) Value() string         { return "" }
func (clickEvent) Checked() bool         { return false }
func (clickEvent) Key() string           { return "" }
func (clickEvent) ClientX() int          { return 0 }
func (clickEvent) ClientY() int          { return 0 }
func (clickEvent) Target() any           { return nil }
func (clickEvent) SetData(_, _ string)   {}
func (clickEvent) GetData(string) string { return "" }

var _ vdom.Event = clickEvent{}
