//go:build dev

package bundler

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"testing"
	"time"
)

// chromePath returns a headless-capable Chrome binary, or "" if none is found.
func chromePath() string {
	candidates := []string{
		"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
		"/Applications/Chromium.app/Contents/MacOS/Chromium",
	}
	for _, name := range []string{"google-chrome", "chromium", "chromium-browser", "google-chrome-stable"} {
		if p, err := exec.LookPath(name); err == nil {
			candidates = append(candidates, p)
		}
	}
	for _, c := range candidates {
		if _, err := os.Stat(c); err == nil {
			return c
		}
	}
	return ""
}

// TestBrowserNativeESMRenders proves the whole native-ESM path works in a real
// browser: the import map resolves solid-js to one vendored instance, the module
// server transforms + serves the entry and a .tsx component (solid-refresh
// compiled), and Solid mounts it reactively. The page reports its rendered text
// back to the test via a beacon (robust against headless Chrome's one-shot exit
// quirks). It does NOT assert the interactive hot-swap — that needs a persistent
// CDP session and is the final manual check — but removes the largest
// browser-integration risk.
func TestBrowserNativeESMRenders(t *testing.T) {
	chrome := chromePath()
	if chrome == "" {
		t.Skip("no headless Chrome/Chromium available")
	}
	if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
		t.Skip("browser smoke test only wired for macOS/Linux")
	}

	root, err := filepath.Abs("../..")
	if err != nil {
		t.Fatal(err)
	}
	t.Chdir(root)
	frontendAbs, _ := filepath.Abs(frontendDir)

	// Fixtures in a temp src root so the real source tree (and Tailwind scan /
	// route gen) is untouched. The component uses JSX (→ solid-refresh) and a
	// signal (→ real reactivity); the entry imports it via a `.js` specifier
	// (→ .tsx resolution), then beacons back the rendered text.
	tmpSrc := evalSymlinks(t.TempDir())
	writeFile(t, filepath.Join(tmpSrc, "Widget.tsx"),
		`import { createSignal } from "solid-js";
export default function Widget() {
  const [msg] = createSignal("hello-hmr-rendered");
  return <div id="w">{msg()}</div>;
}
`)
	writeFile(t, filepath.Join(tmpSrc, "entry.tsx"),
		`import { render } from "solid-js/web";
import Widget from "./Widget.js";
render(() => <Widget/>, document.getElementById("app"));
setTimeout(() => {
  const el = document.getElementById("w");
  fetch("/__result?text=" + encodeURIComponent(el ? el.textContent : "EMPTY"));
}, 0);
`)

	eps, err := loadVendorManifest(filepath.Join(frontendAbs, "vendor"))
	if err != nil {
		t.Fatalf("vendor manifest: %v", err)
	}
	d := &devServer{
		hub:               newHub(),
		frontend:          frontendAbs,
		srcRoot:           tmpSrc,
		vendor:            filepath.Join(frontendAbs, "vendor"),
		graph:             newModuleGraph(),
		vendorCache:       map[string][]byte{},
		cssTrigger:        make(chan struct{}, 1),
		vendorEntrypoints: eps,
	}
	d.importMapJSON = d.buildImportMap()

	rendered := make(chan string, 1)
	var diagMu sync.Mutex
	var diag []string
	mux := http.NewServeMux()
	d.register(mux)
	mux.HandleFunc("/__result", func(w http.ResponseWriter, r *http.Request) {
		select {
		case rendered <- r.URL.Query().Get("text"):
		default:
		}
		w.WriteHeader(http.StatusNoContent)
	})
	mux.HandleFunc("/__diag", func(w http.ResponseWriter, r *http.Request) {
		diagMu.Lock()
		diag = append(diag, r.URL.Query().Get("text"))
		diagMu.Unlock()
		w.WriteHeader(http.StatusNoContent)
	})
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/" {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprintf(w, `<!DOCTYPE html><html><head>
<script>
function beacon(p,t){fetch(p+'?text='+encodeURIComponent(t));}
window.addEventListener('error', function(e){beacon('/__diag','ERR:'+((e.error&&e.error.stack)||e.message));});
window.addEventListener('unhandledrejection', function(e){beacon('/__diag','REJ:'+((e.reason&&e.reason.stack)||String(e.reason)));});
window.addEventListener('DOMContentLoaded', function(){beacon('/__diag','DOMCONTENTLOADED');});
</script>
<script type="importmap">%s</script>
</head><body><div id="app"></div>
<script type="module" src="/@src/entry.tsx"></script>
</body></html>`, d.importMapJSON)
	})
	srv := httptest.NewServer(mux)
	defer srv.Close()

	// Sanity-check that every module the page needs serves (200) before the browser.
	for _, m := range []string{"/@src/entry.tsx", "/@src/Widget.tsx", "/@hmr/client",
		vendorURLPrefix + "solid-js.js", vendorURLPrefix + "solid-js/web.js"} {
		resp, err := http.Get(srv.URL + m)
		if err != nil {
			t.Fatalf("GET %s: %v", m, err)
		}
		resp.Body.Close()
		if resp.StatusCode != 200 {
			t.Fatalf("GET %s -> %d", m, resp.StatusCode)
		}
	}

	// Launch headless Chrome to load the page; wait for the render beacon rather
	// than for Chrome to exit (new headless doesn't reliably one-shot-exit here).
	// Use a best-effort temp profile dir (not t.TempDir): Chrome's detached helper
	// processes may still be writing to it at cleanup time, and t.TempDir's strict
	// RemoveAll would then fail the test.
	userDir, _ := os.MkdirTemp("", "hmr-chrome-*")
	defer os.RemoveAll(userDir)
	devNull, _ := os.Open(os.DevNull)
	defer devNull.Close()

	ctx, cancel := context.WithCancel(context.Background())
	cmd := exec.CommandContext(ctx, chrome,
		"--headless=new", "--disable-gpu", "--no-sandbox", "--no-first-run",
		"--user-data-dir="+userDir,
		"--disable-background-networking", "--disable-component-update",
		"--disable-default-apps", "--disable-sync", "--no-default-browser-check",
		srv.URL,
	)
	cmd.Stdout = devNull
	cmd.Stderr = devNull
	if err := cmd.Start(); err != nil {
		cancel()
		t.Fatalf("start chrome: %v", err)
	}
	defer func() { cancel(); cmd.Wait() }()

	select {
	case text := <-rendered:
		if text != "hello-hmr-rendered" {
			t.Fatalf("browser rendered %q, want %q", text, "hello-hmr-rendered")
		}
		t.Log("native-ESM app rendered in headless Chrome (import map + single solid-js + solid-refresh component)")
	case <-time.After(25 * time.Second):
		diagMu.Lock()
		msgs := strings.Join(diag, "\n  ")
		diagMu.Unlock()
		t.Fatalf("timed out waiting for the browser render beacon.\nbrowser diagnostics:\n  %s", msgs)
	}
}

func writeFile(t *testing.T, path, content string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
}
