package state

import (
	"io"
	"log/slog"
	"testing"

	modulepkg "github.com/dbledeez/panel/pkg/module"
	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

type captureEmitter struct {
	appStates    []*panelv1.AppStateUpdate
	playerEvents []*panelv1.PlayerEvent
}

func (c *captureEmitter) EmitAppState(a *panelv1.AppStateUpdate)    { c.appStates = append(c.appStates, a) }
func (c *captureEmitter) EmitPlayerEvent(p *panelv1.PlayerEvent)    { c.playerEvents = append(c.playerEvents, p) }

func newTestTracker(t *testing.T, m *modulepkg.Manifest) (*Tracker, *captureEmitter) {
	t.Helper()
	cap := &captureEmitter{}
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	tr, err := New(log, Config{
		InstanceID: "inst-1",
		Manifest:   m,
		Emitter:    cap,
	})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	return tr, cap
}

func Test7DTDJoinPattern(t *testing.T) {
	manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
	if err != nil {
		t.Fatalf("load 7dtd: %v", err)
	}
	tr, cap := newTestTracker(t, manifest)

	// Real-world 7DTD log line shape
	line := `2026-04-19T10:00:00 42.123 INF PlayerSpawnedInWorld (by system): PltfmId='Steam_76561198000000001', CrossId='EOS_00000000', OwnerID='Steam_76561198000000001', PlayerName='Alice'`
	tr.OnLogLine(line)

	if len(cap.playerEvents) != 1 {
		t.Fatalf("expected 1 player event, got %d", len(cap.playerEvents))
	}
	ev := cap.playerEvents[0]
	if ev.Kind != panelv1.PlayerEvent_KIND_JOIN {
		t.Errorf("kind = %v, want JOIN", ev.Kind)
	}
	if ev.PlayerName != "Alice" {
		t.Errorf("name = %q, want Alice", ev.PlayerName)
	}
	if ev.PlayerId == "" {
		t.Errorf("player_id empty; expected Steam_... from named groups")
	}
}

func Test7DTDChatPattern(t *testing.T) {
	manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
	if err != nil {
		t.Fatalf("load 7dtd: %v", err)
	}
	tr, cap := newTestTracker(t, manifest)

	line := `2026-04-19T10:00:00 42.123 INF Chat (from 'Steam_76561198000000001', entity id '123', to 'Global'): 'Alice': hello world`
	tr.OnLogLine(line)

	if len(cap.playerEvents) != 1 {
		t.Fatalf("expected 1 player event, got %d", len(cap.playerEvents))
	}
	ev := cap.playerEvents[0]
	if ev.Kind != panelv1.PlayerEvent_KIND_CHAT {
		t.Errorf("kind = %v, want CHAT", ev.Kind)
	}
	if ev.PlayerName != "Alice" {
		t.Errorf("name = %q, want Alice", ev.PlayerName)
	}
	if ev.Detail != "hello world" {
		t.Errorf("detail = %q, want 'hello world'", ev.Detail)
	}
}

func Test7DTDLPParse(t *testing.T) {
	manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
	if err != nil {
		t.Fatalf("load 7dtd: %v", err)
	}
	var lp *modulepkg.StateSource
	for i := range manifest.StateSources {
		if manifest.StateSources[i].Type == "rcon" {
			lp = &manifest.StateSources[i]
			break
		}
	}
	if lp == nil {
		t.Fatal("no rcon state source in 7dtd manifest")
	}

	sample := "Total of 3 in the game\n\n1. id=1, Alice, pos=(0,0,0)\n2. id=2, Bob, pos=(1,0,0)\n3. id=3, Carol, pos=(2,0,0)"
	app := parseStateOutput("inst-1", sample, *lp)
	if app == nil {
		t.Fatal("parse returned nil")
	}
	if app.PlayersOnline != 3 {
		t.Errorf("players_online = %d, want 3", app.PlayersOnline)
	}
}

func TestLineWithNoMatchYieldsNoEvent(t *testing.T) {
	manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
	if err != nil {
		t.Fatalf("load 7dtd: %v", err)
	}
	tr, cap := newTestTracker(t, manifest)
	tr.OnLogLine("this is some random log line that matches nothing")
	if len(cap.playerEvents) != 0 {
		t.Errorf("expected 0 events, got %d", len(cap.playerEvents))
	}
}

// ---- WI-05: hoisted state-source regex compilation ----

// TestNewRejectsBadStateSourcePattern: a malformed parse regex must fail at
// construction (surfacing the manifest bug at init) instead of silently
// recompiling-and-failing on every poll tick.
func TestNewRejectsBadStateSourcePattern(t *testing.T) {
	m := &modulepkg.Manifest{
		StateSources: []modulepkg.StateSource{{
			Type:    "rcon",
			Command: "lp",
			Parse:   &modulepkg.ParseConfig{Kind: "regex", Pattern: "("},
		}},
	}
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	if _, err := New(log, Config{InstanceID: "i", Manifest: m, Emitter: &captureEmitter{}}); err == nil {
		t.Fatal("New accepted an invalid state-source regex; want construction error")
	}
}

// TestParseStateUsesPrecompiledRegex drives the tracker's precompiled parse
// path with the real 7DTD manifest and confirms it matches the uncached
// package-level parser.
func TestParseStateUsesPrecompiledRegex(t *testing.T) {
	manifest, err := modulepkg.LoadFile("../../../modules/7dtd/module.yaml")
	if err != nil {
		t.Fatalf("load 7dtd: %v", err)
	}
	tr, _ := newTestTracker(t, manifest)
	var lp *modulepkg.StateSource
	for i := range manifest.StateSources {
		if manifest.StateSources[i].Type == "rcon" {
			lp = &manifest.StateSources[i]
			break
		}
	}
	if lp == nil {
		t.Fatal("no rcon state source in 7dtd manifest")
	}
	sample := "Total of 3 in the game\n\n1. id=1, Alice, pos=(0,0,0)\n2. id=2, Bob, pos=(1,0,0)\n3. id=3, Carol, pos=(2,0,0)"
	app := tr.parseState(sample, *lp)
	if app == nil {
		t.Fatal("precompiled parseState returned nil")
	}
	if app.PlayersOnline != 3 {
		t.Errorf("players_online = %d, want 3", app.PlayersOnline)
	}
}
