//go:build dev

package bundler

import (
	"encoding/json"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// A `?url` import must resolve to a shim module whose default export is the raw
// /@fs/ file URL — not the file itself (which would fail with "no default export",
// as the pdfjs worker did and cascaded into a stuck loading spinner).
func TestDevServerAssetURL(t *testing.T) {
	root, err := filepath.Abs("../..")
	if err != nil {
		t.Fatal(err)
	}
	t.Chdir(root)
	frontendAbs, _ := filepath.Abs(frontendDir)

	tmpSrc := evalSymlinks(t.TempDir())
	os.WriteFile(filepath.Join(tmpSrc, "uses-worker.tsx"),
		[]byte(`import u from "pdfjs-dist/build/pdf.worker.min.mjs?url";
export const url = u;`), 0o644)

	eps, _ := loadVendorManifest(filepath.Join(frontendAbs, "vendor"))
	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}

	out, err := d.transformModule(filepath.Join(tmpSrc, "uses-worker.tsx"))
	if err != nil {
		t.Fatalf("transform: %v", err)
	}
	const shimURL = "/@url/vendor/pdfjs-dist/build/pdf.worker.min.mjs"
	if !strings.Contains(string(out), shimURL) {
		t.Fatalf("?url import not rewritten to the shim path %q:\n%s", shimURL, out)
	}

	req := httptest.NewRequest("GET", shimURL, nil)
	w := httptest.NewRecorder()
	d.serveAssetURL(w, req)
	body := w.Body.String()
	const want = `export default "/@fs/vendor/pdfjs-dist/build/pdf.worker.min.mjs"`
	if w.Code != 200 || !strings.Contains(body, want) {
		t.Fatalf("shim module = %q (status %d), want default export of the /@fs/ URL", body, w.Code)
	}
}

// resolveSource must mirror esbuild's resolution: a `.js` specifier for a sibling
// .ts/.tsx, extensionless specifiers, and directory index files.
func TestResolveSourceExtensions(t *testing.T) {
	dir := t.TempDir()
	write := func(rel string) {
		p := filepath.Join(dir, rel)
		os.MkdirAll(filepath.Dir(p), 0o755)
		os.WriteFile(p, []byte("export default 1;"), 0o644)
	}
	write("Foo.tsx")
	write("Bar.ts")
	write("baz/index.ts")
	write("Real.js")

	cases := []struct{ spec, want string }{
		{"./Foo.js", "Foo.tsx"},   // .js specifier -> sibling .tsx
		{"./Foo.tsx", "Foo.tsx"},  // exact
		{"./Bar", "Bar.ts"},       // extensionless
		{"./baz", "baz/index.ts"}, // directory index
		{"./Real.js", "Real.js"},  // real .js wins
		{"./missing.js", ""},      // dangling
	}
	for _, c := range cases {
		got := resolveSource(dir, c.spec)
		want := ""
		if c.want != "" {
			want = filepath.Join(dir, filepath.FromSlash(c.want))
		}
		if got != want {
			t.Errorf("resolveSource(%q) = %q, want %q", c.spec, got, want)
		}
	}
}

// End-to-end over the dev server's HTTP surface: the SPA entry, a .tsx component,
// a vendor bundle, the client runtime, the import map, and graph-driven update vs.
// full-reload decisions — all without a database or the full server.
func TestDevServerEndToEnd(t *testing.T) {
	root, err := filepath.Abs("../..")
	if err != nil {
		t.Fatal(err)
	}
	t.Chdir(root)

	d, err := newDevServer()
	if err != nil {
		t.Fatalf("newDevServer: %v", err)
	}

	get := func(path string) (int, string) {
		req := httptest.NewRequest("GET", path, nil)
		w := httptest.NewRecorder()
		switch {
		case strings.HasPrefix(path, srcURLPrefix):
			d.serveModule(w, req)
		case strings.HasPrefix(path, vendorURLPrefix):
			d.serveVendor(w, req)
		case path == "/@hmr/client":
			d.serveClient(w, req)
		}
		return w.Code, w.Body.String()
	}

	// --- SPA entry: hot bootstrap + bare imports kept + relative imports rewritten
	code, app := get(srcURLPrefix + "app.ts")
	if code != 200 {
		t.Fatalf("app.ts status %d:\n%s", code, app)
	}
	for _, want := range []string{
		`__createHotContext("/@src/app.ts")`, // hot bootstrap
		`/@hmr/client`,                       // client import injected
		`"solid-js/web"`,                     // bare specifier preserved for import map
		`"@solidjs/router"`,                  // bare specifier preserved
		`/@src/routes/app-routes.ts`,         // relative import rewritten
		`/@src/layouts/AppLayout.ts`,         // relative import rewritten
	} {
		if !strings.Contains(app, want) {
			t.Errorf("app.ts missing %q", want)
		}
	}

	// --- a .tsx component: Solid + solid-refresh instrumentation
	code, alerts := get(srcURLPrefix + "ui/Alerts.tsx")
	if code != 200 {
		t.Fatalf("Alerts.tsx status %d:\n%s", code, alerts)
	}
	for _, want := range []string{
		`__createHotContext("/@src/ui/Alerts.tsx")`,
		`solid-refresh`,   // runtime import
		"import.meta.hot", // esm HMR accept
	} {
		if !strings.Contains(alerts, want) {
			t.Errorf("Alerts.tsx missing %q", want)
		}
	}

	// --- vendor: solid-js/web must import "solid-js" externally (single instance)
	code, web := get(vendorURLPrefix + "solid-js/web.js")
	if code != 200 {
		t.Fatalf("vendor solid-js/web status %d", code)
	}
	if !strings.Contains(web, `"solid-js"`) {
		t.Errorf("solid-js/web should keep a bare `solid-js` import (shared instance)")
	}

	// --- client runtime is an ES module exporting createHotContext
	code, client := get("/@hmr/client")
	if code != 200 || !strings.Contains(client, "export function createHotContext") {
		t.Errorf("client runtime missing createHotContext (status %d)", code)
	}

	// --- import map maps the key vendored specifiers to /@vendor/ URLs
	var im struct {
		Imports map[string]string `json:"imports"`
	}
	if err := json.Unmarshal([]byte(d.importMapJSON), &im); err != nil {
		t.Fatalf("import map JSON: %v", err)
	}
	for _, spec := range []string{"solid-js", "solid-js/web", "@solidjs/router", "solid-refresh"} {
		if got := im.Imports[spec]; got != vendorURLPrefix+spec+".js" {
			t.Errorf("import map[%q] = %q, want %q", spec, got, vendorURLPrefix+spec+".js")
		}
	}

	// --- graph: editing a routes table (non-boundary, reachable from the entry)
	//     forces a full reload; editing a .tsx component is a hot update.
	routes := filepath.Join(d.srcRoot, "routes", "app-routes.ts")
	if _, _, reload := d.graph.invalidate(routes); !reload {
		t.Errorf("changing app-routes.ts should force a full reload")
	}
	alertsPath := filepath.Join(d.srcRoot, "ui", "Alerts.tsx")
	if boundaries, _, reload := d.graph.invalidate(alertsPath); reload || len(boundaries) == 0 {
		t.Errorf("changing Alerts.tsx should be a hot update, got reload=%v boundaries=%v", reload, boundaries)
	}
}
