package jsbundler

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// walkRepoTSX returns every .tsx/.jsx under frontend/src (relative to the
// package dir, which is jsbundler).
func walkRepoTSX(t *testing.T) []string {
	t.Helper()
	root := filepath.Join("..", "..", "frontend", "src")
	var files []string
	err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		switch filepath.Ext(p) {
		case ".tsx", ".jsx":
			files = append(files, p)
		}
		return nil
	})
	if err != nil {
		t.Skipf("cannot walk frontend/src: %v", err)
	}
	if len(files) == 0 {
		t.Skip("no .tsx files found")
	}
	return files
}

// Segmentation must be lossless for every real component in the repo, and the
// join invariant must hold exactly (byte-for-byte).
func TestSegmentLosslessRepo(t *testing.T) {
	files := walkRepoTSX(t)
	totalChunks, multiChunkFiles := 0, 0
	for _, f := range files {
		data, err := os.ReadFile(f)
		if err != nil {
			t.Fatalf("read %s: %v", f, err)
		}
		src := string(data)
		chunks := segmentTopLevel(src)
		if strings.Join(chunks, "") != src {
			t.Errorf("%s: segmentation not lossless (%d chunks)", f, len(chunks))
			continue
		}
		totalChunks += len(chunks)
		if len(chunks) > 1 {
			multiChunkFiles++
		}
		// Every chunk (except possibly the first) must begin at a declaration
		// keyword after optional leading blank lines/comments were attached to
		// the previous chunk — i.e. its first non-space line starts with a kw.
	}
	t.Logf("segmented %d files: %d split into >1 chunk, %d chunks total (avg %.1f)",
		len(files), multiChunkFiles, totalChunks, float64(totalChunks)/float64(len(files)))
}

// Focused check on the pain file: it should split into many chunks so that
// editing any single declaration recompiles only that declaration.
func TestSegmentAutoTable(t *testing.T) {
	p := filepath.Join("..", "..", "frontend", "src", "ui", "AutoTable.tsx")
	data, err := os.ReadFile(p)
	if err != nil {
		t.Skipf("cannot read AutoTable: %v", err)
	}
	chunks := segmentTopLevel(string(data))
	if strings.Join(chunks, "") != string(data) {
		t.Fatal("AutoTable segmentation not lossless")
	}
	// Size distribution: how big is the largest chunk (the residual worst case)?
	maxLen, maxIdx := 0, 0
	for i, c := range chunks {
		if len(c) > maxLen {
			maxLen, maxIdx = len(c), i
		}
	}
	head := strings.TrimSpace(chunks[maxIdx])
	if len(head) > 80 {
		head = head[:80]
	}
	t.Logf("AutoTable: %d chunks, largest = %d bytes (%.0f%% of file), starts: %q",
		len(chunks), maxLen, 100*float64(maxLen)/float64(len(data)), head)
	if len(chunks) < 20 {
		t.Errorf("expected AutoTable to split into many chunks, got %d", len(chunks))
	}
}

// A few hand-written cases exercise the lexer edge cases the repo may not cover.
func TestSegmentEdgeCases(t *testing.T) {
	cases := map[string]string{
		"template with braces and decl-looking text": "const a = `x${ {y:1} }z\nconst notReal = 2`;\nexport const b = 3;\n",
		"regex with braces":                          "const re = /[{}]/g;\nfunction f() { return /a{2}/; }\nexport function g() {}\n",
		"block comment spanning decl keyword":        "/*\nconst hidden = 1;\nfunction alsoHidden() {}\n*/\nexport const real = 1;\n",
		"string with keyword":                        "const s = \"export function fake() {}\";\nfunction real() {}\n",
		"nested template":                            "const t = `a${`b${1}c`}d`;\nexport const u = 1;\n",
	}
	for name, src := range cases {
		chunks := segmentTopLevel(src)
		if got := strings.Join(chunks, ""); got != src {
			t.Errorf("%s: not lossless\n  src=%q\n  got=%q", name, src, got)
		}
	}
}
