package conformance_test

import (
	"strings"
	"testing"

	"github.com/charmbracelet/x/vt"
	"go.mitchellh.com/libghostty"
)

// Two reference emulators, because they disagree about how wide a grapheme
// cluster is and that disagreement is the whole subject of these tests.
//
// libghostty is ghostty's real VT engine. It models legacy widths per
// codepoint, which is what stock terminals actually do, and it implements DEC
// mode 2027 so both width models can be exercised. x/vt always measures whole
// clusters, so it reproduces a mode-2027 terminal only.
//
// Neither is redundant. A renderer bug that only shows up under legacy widths
// is invisible to x/vt, and keeping both means a disagreement between them is
// itself a signal worth investigating.

// oracle is a terminal emulator the renderer can write to and whose screen can
// be read back column by column.
//
// Reading by column is the point. A formatter that returns the screen as a
// string collapses the spacer cells that follow a wide glyph, which is exactly
// the information these tests need: it cannot distinguish a cluster at column 0
// with a marker at column 2 from the same cluster with a marker at column 1.
type oracle interface {
	// Write feeds renderer output into the emulator.
	Write(p []byte) (n int, err error)

	// Row returns the visible text of a row, trailing blanks removed.
	Row(t *testing.T, y int) string

	// Size reports the emulator's dimensions.
	Size() (w, h int)

	// Resize changes the emulator's dimensions, mirroring a SIGWINCH.
	Resize(w, h int)

	// Close releases emulator resources.
	Close()

	// Name identifies the emulator in failure messages.
	Name() string
}

// ghosttyOracle wraps ghostty's VT engine.
type ghosttyOracle struct {
	term *libghostty.Terminal
	w, h int
}

// newGhostty creates a ghostty emulator. When grapheme is false it measures
// clusters with legacy per-codepoint widths, the way an ordinary terminal that
// has not negotiated mode 2027 does.
func newGhostty(t *testing.T, w, h int, grapheme bool) oracle {
	t.Helper()

	term, err := libghostty.NewTerminal(libghostty.WithSize(uint16(w), uint16(h)))
	if err != nil {
		t.Fatalf("libghostty.NewTerminal(%d, %d): %v", w, h, err)
	}
	if err := term.ModeSet(libghostty.ModeGraphemeCluster, grapheme); err != nil {
		term.Close()
		t.Fatalf("ModeSet(ModeGraphemeCluster, %v): %v", grapheme, err)
	}
	return &ghosttyOracle{term: term, w: w, h: h}
}

func (g *ghosttyOracle) Write(p []byte) (int, error) { return g.term.Write(p) }
func (g *ghosttyOracle) Size() (int, int)            { return g.w, g.h }
func (g *ghosttyOracle) Close()                      { g.term.Close() }
func (g *ghosttyOracle) Name() string                { return "ghostty" }

func (g *ghosttyOracle) Resize(w, h int) {
	g.w, g.h = w, h
	// Cell pixel dimensions are irrelevant to these tests, so pass zero.
	_ = g.term.Resize(uint16(w), uint16(h), 0, 0)
}

func (g *ghosttyOracle) Row(t *testing.T, y int) string {
	t.Helper()

	var b strings.Builder
	for x := range g.w {
		ref, err := g.term.GridRef(libghostty.Point{
			Tag: libghostty.PointTagActive,
			X:   uint16(x),
			Y:   uint32(y),
		})
		if err != nil {
			t.Fatalf("GridRef(%d, %d): %v", x, y, err)
		}
		cell, err := ref.Cell()
		if err != nil {
			t.Fatalf("Cell(%d, %d): %v", x, y, err)
		}

		// Graphemes returns the whole cluster including its base codepoint, so
		// reading Codepoint as well would duplicate the base. Fall back to
		// Codepoint only for cells that hold a single rune.
		if cps, err := ref.Graphemes(); err == nil && len(cps) > 0 {
			for _, cp := range cps {
				b.WriteRune(rune(cp))
			}
			continue
		}
		if cp, err := cell.Codepoint(); err == nil && cp != 0 {
			b.WriteRune(rune(cp))
		}
	}

	// Empty cells read back as NUL rather than a space, so trim both.
	return strings.TrimRight(b.String(), " \x00")
}

// vtOracle wraps x/vt, which always measures whole grapheme clusters and so
// behaves like a terminal with mode 2027 enabled.
type vtOracle struct {
	em *vt.Emulator
}

func newVT(t *testing.T, w, h int) oracle {
	t.Helper()
	return &vtOracle{em: vt.NewEmulator(w, h)}
}

func (v *vtOracle) Write(p []byte) (int, error) { return v.em.Write(p) }
func (v *vtOracle) Size() (int, int)            { return v.em.Width(), v.em.Height() }
func (v *vtOracle) Close()                      {}
func (v *vtOracle) Name() string                { return "vt" }
func (v *vtOracle) Resize(w, h int)             { v.em.Resize(w, h) }

func (v *vtOracle) Row(t *testing.T, y int) string {
	t.Helper()

	var b strings.Builder
	for x := range v.em.Width() {
		if cell := v.em.CellAt(x, y); cell != nil {
			b.WriteString(cell.Content)
		}
	}
	return strings.TrimRight(b.String(), " ")
}
