package app

import (
	"bytes"
	"os"
	"strings"
	"testing"

	"kjol/wasmruntime/vdom"
	"kjol/webui"
)

func TestSSRPages(t *testing.T) {
	for _, path := range []string{"/", "/about", "/wasm/chart", "/wasm/data", "/wasm/components"} {
		deps := Deps{Path: func() string { return path }}
		html := vdom.RenderHTML(Shell(deps, Routes(deps)))
		t.Logf("%-8s %6d bytes  portals=%d", path, len(html), strings.Count(html, "data-portal"))
		if len(html) < 200 {
			t.Errorf("%s rendered only %d bytes", path, len(html))
		}
	}
}

func TestSSRTablePage(t *testing.T) {
	deps := Deps{Path: func() string { return "/wasm/components" }}
	html := vdom.RenderHTML(Shell(deps, Routes(deps)))

	// The table persists a personal layout in localStorage, which the SERVER CANNOT
	// READ. So the server renders a SKELETON, not the default table: if it rendered
	// the default one, a user who had reordered their columns would watch them
	// rearrange themselves once the wasm booted.
	//
	// This is a real cost — the page ships no table content — and it is the price of
	// never showing the wrong table. See webui.RestoreLayout.
	if !strings.Contains(html, `aria-busy="true"`) {
		t.Error("SSR should render the AutoTable's loading skeleton, not a table")
	}
	if !strings.Contains(html, "animate-pulse") {
		t.Error("the skeleton bars are missing")
	}

	// A salary, which ONLY the AutoTable renders.
	//
	// This used to look for "Ada Lovelace", which was a fine proxy back when the table
	// had a page to itself. It is not one any more: the components page also demos
	// PrettyTable, and PrettyTable's rows are Ada, Alan and Grace — so the old assertion
	// failed on a page that was behaving perfectly. A test that names a value only the
	// component under test can produce cannot be fooled by its neighbours.
	if strings.Contains(html, "$1,610.25") {
		t.Error("SSR rendered AutoTable CONTENT — a user with a saved layout would watch it rearrange")
	}
}

// renderedTable drives the very table the page renders, past its skeleton. Natively
// there is nothing to restore, so RestoreLayout just marks the layout settled.
func renderedTable(t *testing.T) string {
	t.Helper()
	highlight := vdom.NewSignal("")
	table := newEmployeeTable(highlight)
	table.SetRows(employees())
	table.RestoreLayout()
	return vdom.RenderHTML(table.Render())
}

// Once the layout has settled, the table renders in full.
func TestTableRendersOnceSettled(t *testing.T) {
	html := renderedTable(t)

	for _, want := range []string{"Ada Lovelace", "Salary"} {
		if !strings.Contains(html, want) {
			t.Errorf("settled table missing %q", want)
		}
	}
	// PerPage is 5, so page one holds 5 of the 12 rows.
	if got := strings.Count(html, "@example.com"); got != 5 {
		t.Errorf("rendered %d rows, want 5 (one page)", got)
	}
	// The Rank column is HiddenByDefault.
	if strings.Contains(html, ">Rank<") {
		t.Error("a HiddenByDefault column was rendered")
	}
	if !strings.Contains(html, "Page 1 of 3") {
		t.Error("pagination did not compute 3 pages for 12 rows at 5/page")
	}
}

// Calculated columns, end to end through the page, in all three shapes.
//
// Page 1 (declared order):
//
//	salary    1200.50  1500.00   980.00  1340.00  1610.25
//	bonus      150.00   300.00     0.00   220.00   400.00
func TestSSRCalculatedColumns(t *testing.T) {
	html := renderedTable(t)

	// BASIC: sum over the operand columns [Salary, Bonus], combined ACROSS each row.
	// If this ever aggregated DOWN the column instead, every row would read the same
	// number — which is exactly the bug these values are here to catch.
	for _, want := range []string{"$1,350.50", "$1,800.00", "$980.00", "$1,560.00", "$2,010.25"} {
		if !strings.Contains(html, want) {
			t.Errorf("Total comp missing %s (a per-row Salary + Bonus)", want)
		}
	}

	// ADVANCED: ([Salary] + [Bonus]) * 12.
	for _, want := range []string{"$16,206.00", "$21,600.00", "$11,760.00"} {
		if !strings.Contains(html, want) {
			t.Errorf("Annual column missing %s", want)
		}
	}

	// ADVANCED, position-dependent: SUM({Salary:1:ROW()}) accumulates down the rows.
	for _, want := range []string{"$2,700.50", "$3,680.50", "$5,020.50", "$6,630.75"} {
		if !strings.Contains(html, want) {
			t.Errorf("running total missing %s", want)
		}
	}

	// SUMMARY: aggregated DOWN the column, over ALL 12 filtered rows — not the 5 on
	// this page. 1200.50+1500+980+1340+1610.25+1120+1275.75+1050+1400+860+1180+990.
	if !strings.Contains(html, "$14,506.50") {
		t.Error("footer did not total the whole filtered set ($14,506.50)")
	}
	if !strings.Contains(html, "Average salary") {
		t.Error("summary row label missing")
	}
}

// The export path, driven through the very table the /table page renders.
//
// Export must write what the FILTER selected — every matching row across every page
// — not the five rows on screen; the columns the user can SEE, in their order; and
// the calculated columns, with each row's own value.
func TestTableExport(t *testing.T) {
	highlight := vdom.NewSignal("")
	table := newEmployeeTable(highlight)
	table.RestoreLayout() // nothing to restore natively; reveals the table over its skeleton
	table.SetRows(employees())

	// Filter to one team, then render (which resolves FilteredRows).
	table.SetSearchValue("Team", "Research", true)
	table.Render()

	csv := string(webui.ExportCSV(table.ExportColumns(), table.FilteredRows(), nil))

	// PerPage is 5 and Research has 4 members, but the point is that export ignores
	// paging entirely: every filtered row, no one else's.
	for _, want := range []string{"Alan Turing", "Katherine Johnson", "Barbara Liskov", "Evelyn Boyd Granville"} {
		if !strings.Contains(csv, want) {
			t.Errorf("CSV missing filtered row %q", want)
		}
	}
	if strings.Contains(csv, "Ada Lovelace") {
		t.Error("CSV contains a row the filter excluded")
	}
	// Rank is HiddenByDefault, so it must not be exported.
	if strings.Contains(csv, "Item 10") {
		t.Error("CSV exported a hidden column")
	}
	// The calculated columns come along, and the running total ACCUMULATES —
	// $1,500.00 then $2,840.00 (Turing + Johnson), not the same number twice.
	if !strings.Contains(csv, "Running total") || !strings.Contains(csv, "$2,840.00") {
		t.Errorf("running total did not accumulate in the export:\n%s", csv)
	}

	// And the PDF: a real file, with the same filtered content.
	pdf := table.ExportPDFBytes(webui.AutoTablePDFHeader{
		Title: "Employees", ShowDate: true, Orientation: webui.PDF_ORIENTATION_LANDSCAPE,
	})
	if !bytes.HasPrefix(pdf, []byte("%PDF-")) || !bytes.Contains(pdf, []byte("%%EOF")) {
		t.Fatalf("PDF is not a PDF (%d bytes)", len(pdf))
	}
	if out := os.Getenv("PDF_OUT"); out != "" {
		if err := os.WriteFile(out, pdf, 0o644); err != nil {
			t.Fatal(err)
		}
		t.Logf("wrote %s (%d bytes)", out, len(pdf))
	}
}
