package main

import (
	"io"
	"net"
	"testing"
	"time"

	"go.bug.st/serial"

	"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
	"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
	"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
)

func TestPrefixLines(t *testing.T) {
	tests := []struct {
		name   string
		in     string
		prefix string
		want   string
	}{
		{name: "empty", in: "", prefix: "X ", want: ""},
		{name: "no-prefix", in: "a\n", prefix: "", want: "a\n"},
		{name: "single-line", in: "abc", prefix: "T ", want: "T abc"},
		{name: "multi-line", in: "a\nb\n", prefix: "P ", want: "P a\nP b\n"},
	}

	for _, tt := range tests {
		got := prefixLines(tt.in, tt.prefix)
		if got != tt.want {
			t.Fatalf("%s: prefixLines got=%q want=%q", tt.name, got, tt.want)
		}
	}
}

func TestAppUIEvents(t *testing.T) {
	a := &App{uiEvents: make(chan event.UIEvent, 8)}
	a.SetUIEnabled(true)

	a.Notifyf("hello %s", "world")
	a.Statusf("ok")
	a.ShowModal("Title", "Body")

	ev1 := mustReadEvent(t, a.uiEvents)
	if ev1.Kind != event.UIEventOutput || ev1.Text != "hello world" {
		t.Fatalf("unexpected output event: %+v", ev1)
	}

	ev2 := mustReadEvent(t, a.uiEvents)
	if ev2.Kind != event.UIEventStatus || ev2.Text != "ok" {
		t.Fatalf("unexpected status event: %+v", ev2)
	}

	ev3 := mustReadEvent(t, a.uiEvents)
	if ev3.Kind != event.UIEventModal || ev3.Title != "Title" || ev3.Text != "Body" {
		t.Fatalf("unexpected modal event: %+v", ev3)
	}
}

func TestSendLine(t *testing.T) {
	setupTestPipes()
	a := &App{
		cfg:      &Config{endStr: "\r\n"},
		plugins:  luaplugin.NewManager(),
		uiEvents: make(chan event.UIEvent, 8),
		done:     make(chan struct{}),
	}
	a.SetUIEnabled(true)

	if err := a.sendLine("hello"); err != nil {
		t.Fatalf("sendLine failed: %v", err)
	}

	if err := a.sendLine(""); err != nil {
		t.Fatalf("sendLine empty string should be no-op: %v", err)
	}
	if err := a.sendLine("   "); err != nil {
		t.Fatalf("sendLine whitespace should be no-op: %v", err)
	}
}

func TestHandleLine(t *testing.T) {
	setupTestPipes()
	a := &App{
		cfg:      &Config{endStr: "\n", inputCode: "UTF-8", outputCode: "UTF-8"},
		plugins:  luaplugin.NewManager(),
		uiEvents: make(chan event.UIEvent, 8),
		done:     make(chan struct{}),
	}
	a.SetUIEnabled(true)
	a.forward = forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {})
	a.dispatcher = NewCommandDispatcher(a)

	a.handleLine("hello")
	a.handleLine("")
	a.handleLine(".help")

	ev := mustReadEvent(t, a.uiEvents)
	if ev.Kind != event.UIEventModal || ev.Title == "" {
		t.Fatalf("expected .help modal, got %+v", ev)
	}
}

func TestEmitNonUI(t *testing.T) {
	oldOut := out
	out = io.Discard
	defer func() { out = oldOut }()

	a := &App{
		uiEvents: make(chan event.UIEvent, 4),
		logFile:  nil,
	}
	a.SetUIEnabled(false)

	a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: "serial data\n"})
	a.emit(event.UIEvent{Kind: event.UIEventStatus, Text: "status msg"})
	a.emit(event.UIEvent{Kind: event.UIEventModal, Title: "T", Text: "body"})
	a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: ""})
}

func TestEmitUISaturation(t *testing.T) {
	a := &App{
		uiEvents: make(chan event.UIEvent, 2),
	}
	a.SetUIEnabled(true)

	// Fill channel
	a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: "a"})
	a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: "b"})
	// This should drop oldest and insert newest
	a.emit(event.UIEvent{Kind: event.UIEventOutput, Text: "c"})

	ev := mustReadEvent(t, a.uiEvents)
	if ev.Text != "b" {
		t.Fatalf("expected b after drop, got %q", ev.Text)
	}
	ev = mustReadEvent(t, a.uiEvents)
	if ev.Text != "c" {
		t.Fatalf("expected c, got %q", ev.Text)
	}
}

func TestAppClose(t *testing.T) {
	a := &App{
		done:     make(chan struct{}),
		plugins:  luaplugin.NewManager(),
		forward:  forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}),
		uiEvents: make(chan event.UIEvent, 4),
	}
	a.SetUIEnabled(true)

	a.Close()
	if !a.isClosed() {
		t.Fatalf("expected app closed")
	}
	// Second close should be safe
	a.Close()
}

func TestLoadConfiguredForwards(t *testing.T) {
	oldCfg := config
	defer func() { config = oldCfg }()

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen failed: %v", err)
	}
	defer listener.Close()

	config = Config{
		forWard: []int{int(forward.TCP), int(forward.None), int(forward.UDP)},
		address: []string{listener.Addr().String(), "", ""},
	}

	a := &App{
		cfg:      &config,
		forward:  forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}),
		uiEvents: make(chan event.UIEvent, 8),
		done:     make(chan struct{}),
	}
	a.SetUIEnabled(true)

	a.loadConfiguredForwards()
	// forward.TCP should be added, forward.None skipped, forward.UDP skipped (empty address)
	items := a.forward.List()
	if len(items) != 1 || items[0].Mode != "tcp" {
		t.Fatalf("expected 1 TCP forward, got %+v", items)
	}
}

func TestReportForwardIngress(t *testing.T) {
	a := &App{
		cfg:      &Config{inputCode: "UTF-8", outputCode: "UTF-8"},
		uiEvents: make(chan event.UIEvent, 4),
	}
	a.SetUIEnabled(true)

	a.reportForwardIngress(1, []byte("test"))

	// Hex mode
	a.cfg.inputCode = "hex"
	a.reportForwardIngress(2, []byte{0x41, 0x42})

	// Empty chunk
	a.reportForwardIngress(3, nil)
}

func TestSendCtrl(t *testing.T) {
	oldSp := serialPort
	defer func() { serialPort = oldSp }()

	// Use a mock serial port
	serialPort = &mockSerialPort{}
	a := &App{
		cfg:      &Config{},
		uiEvents: make(chan event.UIEvent, 4),
	}
	a.SetUIEnabled(true)

	if err := a.sendCtrl('c'); err != nil {
		t.Fatalf("sendCtrl('c') failed: %v", err)
	}
	if err := a.sendCtrl('C'); err != nil {
		t.Fatalf("sendCtrl('C') failed: %v", err)
	}
	if err := a.sendCtrl('A'); err != nil {
		t.Fatalf("sendCtrl('A') failed: %v", err)
	}
}

type mockSerialPort struct{}

func (m *mockSerialPort) Write(p []byte) (int, error)     { return len(p), nil }
func (m *mockSerialPort) Read(p []byte) (int, error)      { return 0, io.EOF }
func (m *mockSerialPort) Close() error                    { return nil }
func (m *mockSerialPort) SetMode(mode *serial.Mode) error { return nil }
func (m *mockSerialPort) SetDTR(dtr bool) error           { return nil }
func (m *mockSerialPort) SetRTS(rts bool) error           { return nil }
func (m *mockSerialPort) GetModemStatusBits() (*serial.ModemStatusBits, error) {
	return &serial.ModemStatusBits{}, nil
}
func (m *mockSerialPort) ResetInputBuffer() error              { return nil }
func (m *mockSerialPort) ResetOutputBuffer() error             { return nil }
func (m *mockSerialPort) SetReadTimeout(t time.Duration) error { return nil }
func (m *mockSerialPort) Break(t time.Duration) error          { return nil }
func (m *mockSerialPort) Drain() error                         { return nil }

func mustReadEvent(t *testing.T, ch <-chan event.UIEvent) event.UIEvent {
	t.Helper()
	select {
	case ev := <-ch:
		return ev
	case <-time.After(2 * time.Second):
		t.Fatalf("timed out waiting for UI event")
		return event.UIEvent{}
	}
}
