package gonexapp_test import ( "context" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/chromedp/chromedp" "github.com/chromedp/chromedp/kb" "gitea.neitzel.de/konrad/go-nc-exapp" ) func TestMain(m *testing.M) { srv := httptest.NewServer(dialogFixture()) defer srv.Close() fixtureURL = srv.URL path := browserExecutable() if path == "" { fmt.Fprintln(os.Stderr, "dialog tests need Chrome, Edge, or Chromium (set EXAPP_BROWSER)") os.Exit(1) } opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(path), chromedp.WindowSize(1200, 800), ) var cancel context.CancelFunc allocCtx, cancel = chromedp.NewExecAllocator(context.Background(), opts...) code := m.Run() cancel() os.Exit(code) } var ( fixtureURL string allocCtx context.Context ) func dialogFixture() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /bare", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, "Dialog", gonexapp.DialogHTML(), fixtureControls(), "") }) nav := gonexapp.AppNavigation{ DefaultID: "home", Items: func(*http.Request) []gonexapp.Item { return []gonexapp.Item{{ID: "home", Label: "Home"}} }, Page: func(*http.Request, string) (string, bool) { return fixtureControls(), true }, } mux.Handle("GET /shell", nav.Handler()) mux.Handle("GET /nav", sampleNavigation().Handler()) return mux } func fixtureControls() string { return `

`
}

func browserExecutable() string {
	if p := os.Getenv("EXAPP_BROWSER"); p != "" {
		return p
	}
	candidates := []string{
		"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
		"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
		"/Applications/Chromium.app/Contents/MacOS/Chromium",
	}
	for _, c := range candidates {
		if st, err := os.Stat(c); err == nil && !st.IsDir() {
			return c
		}
	}
	return ""
}

func drive(t *testing.T, path string, actions ...chromedp.Action) {
	t.Helper()
	ctx, cancel := chromedp.NewContext(allocCtx)
	t.Cleanup(cancel)
	ctx, cancel = context.WithTimeout(ctx, 25*time.Second)
	t.Cleanup(cancel)
	steps := append([]chromedp.Action{chromedp.Navigate(fixtureURL + path)}, actions...)
	if err := chromedp.Run(ctx, steps...); err != nil {
		t.Fatal(err)
	}
}

func clickButton(label string) chromedp.Action {
	expr := fmt.Sprintf(`(() => {
	  const btn = [...document.querySelectorAll("#exapp-dialog button")].find((b) => b.textContent.trim() === %q && !b.hidden);
	  if (!btn) return "missing";
	  btn.click();
	  return "clicked";
	})()`, label)
	return chromedp.ActionFunc(func(ctx context.Context) error {
		var result string
		if err := chromedp.Evaluate(expr, &result).Do(ctx); err != nil {
			return err
		}
		if result != "clicked" {
			return fmt.Errorf("button %q: %s", label, result)
		}
		return nil
	})
}

func waitJS(expr, want string, contains bool) chromedp.Action {
	return chromedp.ActionFunc(func(ctx context.Context) error {
		deadline, ok := ctx.Deadline()
		if !ok {
			deadline = time.Now().Add(5 * time.Second)
		}
		var last string
		for {
			var got string
			err := chromedp.Evaluate(expr, &got).Do(ctx)
			if err == nil {
				last = got
				if contains && strings.Contains(got, want) {
					return nil
				}
				if !contains && strings.TrimSpace(got) == want {
					return nil
				}
			}
			if time.Now().After(deadline) {
				return fmt.Errorf("%s = %q, want %q", expr, last, want)
			}
			if err := chromedp.Sleep(40 * time.Millisecond).Do(ctx); err != nil {
				return err
			}
		}
	})
}

func waitDialogText(id, want string) chromedp.Action {
	return waitJS(fmt.Sprintf(`document.getElementById("%s").textContent`, id), want, false)
}

func waitLog(want string) chromedp.Action {
	return waitJS(`document.getElementById("log").textContent`, want, true)
}

func openShows(t *testing.T, path, button, severity, heading, message string) {
	t.Helper()
	drive(t, path,
		chromedp.WaitVisible("#"+button, chromedp.ByQuery),
		chromedp.Click("#"+button, chromedp.ByQuery),
		waitDialogText("exapp-dialog-severity", severity),
		waitDialogText("exapp-dialog-message", message),
		chromedp.ActionFunc(func(ctx context.Context) error {
			var head string
			var hidden bool
			if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-heading").textContent`, &head).Do(ctx); err != nil {
				return err
			}
			if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-heading").hidden`, &hidden).Do(ctx); err != nil {
				return err
			}
			if heading == "" {
				if !hidden {
					return fmt.Errorf("heading visible %q", head)
				}
				return nil
			}
			if hidden || strings.TrimSpace(head) != heading {
				return fmt.Errorf("heading %q hidden %v, want %q", head, hidden, heading)
			}
			var bold int
			if err := chromedp.Evaluate(`document.querySelectorAll("#exapp-dialog b, #exapp-dialog i").length`, &bold).Do(ctx); err != nil {
				return err
			}
			if bold != 0 {
				return fmt.Errorf("dialog interpreted HTML, %d elements", bold)
			}
			var label string
			if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").textContent`, &label).Do(ctx); err != nil {
				return err
			}
			if strings.TrimSpace(label) != "OK" {
				return fmt.Errorf("message button %q, want OK", label)
			}
			return nil
		}),
	)
}

func TestMessageOnBarePage(t *testing.T) {
	openShows(t, "/bare", "open-info", "info", "Notice", "Hello x")
}

func TestMessageOnAppNavigation(t *testing.T) {
	openShows(t, "/shell", "open-info", "info", "Notice", "Hello x")
}

func TestMessageSeveritiesAndDismiss(t *testing.T) {
	cases := []struct {
		button   string
		severity string
		how      string
	}{
		{"open-warning", "warning", "ok"},
		{"open-error", "error", "enter"},
		{"open-info", "info", "escape"},
		{"open-plain", "info", "backdrop"},
	}
	for _, tc := range cases {
		t.Run(tc.how, func(t *testing.T) {
			var dismiss chromedp.Action
			switch tc.how {
			case "ok":
				dismiss = clickButton("OK")
			case "enter":
				dismiss = chromedp.KeyEvent(kb.Enter)
			case "escape":
				dismiss = chromedp.KeyEvent(kb.Escape)
			case "backdrop":
				dismiss = chromedp.MouseClickXY(8, 8)
			}
			drive(t, "/bare",
				chromedp.Click("#"+tc.button, chromedp.ByQuery),
				waitDialogText("exapp-dialog-severity", tc.severity),
				dismiss,
				waitLog("dismissed"),
			)
		})
	}
}

func TestMessageWithoutHeading(t *testing.T) {
	openShows(t, "/bare", "open-plain", "info", "", "Only the message")
}

func TestConfirmYesNoAndDestructive(t *testing.T) {
	t.Run("cancel", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-confirm", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Proceed?"),
			clickButton("Cancel"),
			waitLog("no"),
		)
	})
	t.Run("escape", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-confirm", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Proceed?"),
			chromedp.KeyEvent(kb.Escape),
			waitLog("no"),
		)
	})
	t.Run("backdrop", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-confirm", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Proceed?"),
			chromedp.MouseClickXY(8, 8),
			waitLog("no"),
		)
	})
	t.Run("enter", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-confirm", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Proceed?"),
			chromedp.KeyEvent(kb.Enter),
			waitLog("yes"),
		)
	})
	t.Run("delete", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-delete", chromedp.ByQuery),
			waitDialogText("exapp-dialog-heading", "Remove domain"),
			chromedp.ActionFunc(func(ctx context.Context) error {
				var plain, destructive string
				if err := chromedp.Evaluate(`getComputedStyle(document.getElementById("exapp-dialog-cancel")).backgroundColor`, &plain).Do(ctx); err != nil {
					return err
				}
				if err := chromedp.Evaluate(`getComputedStyle(document.getElementById("exapp-dialog-agree")).backgroundColor`, &destructive).Do(ctx); err != nil {
					return err
				}
				if plain == destructive {
					return fmt.Errorf("destructive button color %q matches Cancel", destructive)
				}
				var text string
				if err := chromedp.Evaluate(`getComputedStyle(document.getElementById("exapp-dialog-agree")).color`, &text).Do(ctx); err != nil {
					return err
				}
				if text == "rgb(255, 255, 255)" || text == destructive {
					return fmt.Errorf("destructive label color %q on background %q", text, destructive)
				}
				var label string
				if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").textContent`, &label).Do(ctx); err != nil {
					return err
				}
				if strings.TrimSpace(label) != "Delete" {
					return fmt.Errorf("agree label %q", label)
				}
				return nil
			}),
			clickButton("Delete"),
			waitLog("yes"),
		)
	})
	t.Run("button word is plain text", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-html-button", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Go?"),
			chromedp.ActionFunc(func(ctx context.Context) error {
				var n int
				var label string
				if err := chromedp.Evaluate(`document.querySelectorAll("#exapp-dialog i").length`, &n).Do(ctx); err != nil {
					return err
				}
				if n != 0 {
					return fmt.Errorf("button word was interpreted as HTML")
				}
				if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").textContent`, &label).Do(ctx); err != nil {
					return err
				}
				if strings.TrimSpace(label) != "Go" {
					return fmt.Errorf("agree label %q", label)
				}
				return nil
			}),
		)
	})
}

func TestPromptValueAndCancel(t *testing.T) {
	t.Run("empty stays inactive", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-prompt", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Folder name"),
			waitDialogText("exapp-dialog-heading", "New folder"),
			chromedp.ActionFunc(func(ctx context.Context) error {
				var disabled bool
				if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").disabled`, &disabled).Do(ctx); err != nil {
					return err
				}
				if !disabled {
					return fmt.Errorf("agree button active on an empty Prompt")
				}
				return chromedp.KeyEvent(kb.Enter).Do(ctx)
			}),
			chromedp.Sleep(200*time.Millisecond),
			chromedp.ActionFunc(func(ctx context.Context) error {
				var open bool
				if err := chromedp.Evaluate(`document.getElementById("exapp-dialog").open`, &open).Do(ctx); err != nil {
					return err
				}
				if !open {
					return fmt.Errorf("Enter closed an empty Prompt")
				}
				return nil
			}),
			clickButton("Cancel"),
			waitLog("cancelled"),
		)
	})
	t.Run("spaces only stay inactive", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-blank", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Name"),
			chromedp.ActionFunc(func(ctx context.Context) error {
				var disabled bool
				var raw string
				if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-agree").disabled`, &disabled).Do(ctx); err != nil {
					return err
				}
				if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-value").value`, &raw).Do(ctx); err != nil {
					return err
				}
				if raw != "   " {
					return fmt.Errorf("starting value %q", raw)
				}
				if !disabled {
					return fmt.Errorf("agree button active for spaces")
				}
				return nil
			}),
		)
	})
	t.Run("prefill trims on agree", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-prefill", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Name"),
			chromedp.ActionFunc(func(ctx context.Context) error {
				var raw string
				if err := chromedp.Evaluate(`document.getElementById("exapp-dialog-value").value`, &raw).Do(ctx); err != nil {
					return err
				}
				if raw != "  hi  " {
					return fmt.Errorf("starting value %q", raw)
				}
				return nil
			}),
			clickButton("OK"),
			waitLog(`value:"hi"`),
		)
	})
	t.Run("inner spaces stay", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-inner", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Name"),
			clickButton("OK"),
			waitLog(`value:"a b"`),
		)
	})
	t.Run("escape cancels", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-prompt", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Folder name"),
			chromedp.KeyEvent(kb.Escape),
			waitLog("cancelled"),
		)
	})
	t.Run("typed value", func(t *testing.T) {
		drive(t, "/bare",
			chromedp.Click("#open-prompt", chromedp.ByQuery),
			waitDialogText("exapp-dialog-message", "Folder name"),
			chromedp.SendKeys("#exapp-dialog-value", "  mail ", chromedp.ByQuery),
			chromedp.KeyEvent(kb.Enter),
			waitLog(`value:"mail"`),
		)
	})
}

func TestOneDialogAtATime(t *testing.T) {
	drive(t, "/bare",
		chromedp.Click("#open-three", chromedp.ByQuery),
		waitDialogText("exapp-dialog-message", "First"),
		chromedp.ActionFunc(func(ctx context.Context) error {
			var n int
			if err := chromedp.Evaluate(`document.querySelectorAll("dialog[open]").length`, &n).Do(ctx); err != nil {
				return err
			}
			if n != 1 {
				return fmt.Errorf("open dialogs %d, want 1", n)
			}
			return nil
		}),
		clickButton("OK"),
		waitDialogText("exapp-dialog-message", "Second"),
		clickButton("OK"),
		waitDialogText("exapp-dialog-message", "Third"),
		clickButton("OK"),
		waitLog("1\n2\n3"),
	)
}