package teamspeak

import (
	"strings"
	"testing"
	"time"

	"github.com/honeybbq/teamspeak-go/commands"
	"github.com/honeybbq/teamspeak-go/transport"
)

func TestHandleCommandLines_Empty(t *testing.T) {
	c := newTestClient(t)
	// Must not panic.
	c.handleCommandLines("")
}

func TestHandleCommandLines_NewlineSeparated(t *testing.T) {
	c := newTestClient(t)
	// Two lines: an error line with a return_code + one notify.
	// We only care that neither path panics and notify fires.
	entered := make(chan struct{}, 2)
	c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} })
	c.rebuildMiddlewareChains()

	line1 := "notifycliententerview clid=1 client_nickname=A" +
		" cid=1 client_type=0 client_servergroups= client_unique_identifier=x"
	line2 := "notifycliententerview clid=2 client_nickname=B" +
		" cid=1 client_type=0 client_servergroups= client_unique_identifier=y"
	c.handleCommandLines(line1 + "\n" + line2)

	for i := range 2 {
		select {
		case <-entered:
		case <-time.After(time.Second):
			t.Errorf("notify not fired for line %d", i)
		}
	}
}

func TestHandleCommandLines_NullByteSeparated(t *testing.T) {
	c := newTestClient(t)
	entered := make(chan struct{}, 1)
	c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} })
	c.rebuildMiddlewareChains()

	line := "notifycliententerview clid=3 client_nickname=C" +
		" cid=1 client_type=0 client_servergroups= client_unique_identifier=z"
	c.handleCommandLines(line + "\x00")

	select {
	case <-entered:
	case <-time.After(time.Second):
		t.Error("notify not fired")
	}
}

func TestHandleCommand_Notify_Routed(t *testing.T) {
	c := newTestClient(t)
	entered := make(chan struct{}, 1)
	c.OnClientEnter(func(_ ClientInfo) { entered <- struct{}{} })
	c.rebuildMiddlewareChains()

	line := "notifycliententerview clid=4 client_nickname=D" +
		" cid=1 client_type=0 client_servergroups= client_unique_identifier=d"
	c.handleCommand(line)

	select {
	case <-entered:
	case <-time.After(time.Second):
		t.Error("notification not routed")
	}
}

func TestHandleCommand_Error_NoReturnCode_NoResolve(t *testing.T) {
	c := newTestClient(t)
	rc, ch := c.cmdTrack.register()

	// error without a matching return_code should not resolve our tracker.
	c.handleCommand("error id=1 msg=fail")

	select {
	case <-ch:
		t.Error("should not resolve for error without matching return_code")
	case <-time.After(50 * time.Millisecond):
	}
	c.cmdTrack.unregister(rc)
}

func TestHandleCommand_Error_WithReturnCode_Resolves(t *testing.T) {
	c := newTestClient(t)
	rc, ch := c.cmdTrack.register()

	c.handleCommand("error id=0 msg=ok return_code=" + mustUint32Str(rc))

	select {
	case result := <-ch:
		if result.Err != nil {
			t.Errorf("expected nil error, got %v", result.Err)
		}
	case <-time.After(time.Second):
		t.Error("command not resolved")
	}
}

func TestHandleCommand_Error_NonZeroID_ReturnsError(t *testing.T) {
	c := newTestClient(t)
	rc, ch := c.cmdTrack.register()

	c.handleCommand("error id=256 msg=notfound return_code=" + mustUint32Str(rc))

	select {
	case result := <-ch:
		if result.Err == nil {
			t.Error("expected non-nil error for id=256")
		}
		if !strings.Contains(result.Err.Error(), "notfound") {
			t.Errorf("unexpected error: %v", result.Err)
		}
	case <-time.After(time.Second):
		t.Error("command not resolved")
	}
}

func TestHandleCommand_DataCollectedBeforeError(t *testing.T) {
	c := newTestClient(t)
	rc, ch := c.cmdTrack.register()

	// In the real TeamSpeak flow, data rows arrive before the "error" response.
	c.handleCommand("somedata key=val")
	c.handleCommand("error id=0 msg=ok return_code=" + mustUint32Str(rc))

	select {
	case result := <-ch:
		if len(result.Data) != 1 || result.Data[0]["key"] != "val" {
			t.Errorf("unexpected data: %v", result.Data)
		}
	case <-time.After(time.Second):
		t.Error("not resolved")
	}
}

func TestHandleCommand_UnknownCommand_CollectsAsData(t *testing.T) {
	c := newTestClient(t)
	rc, ch := c.cmdTrack.register()

	c.handleCommand("unknowncmd foo=bar baz=qux")
	c.handleCommand("error id=0 msg=ok return_code=" + mustUint32Str(rc))

	select {
	case result := <-ch:
		if len(result.Data) != 1 {
			t.Fatalf("expected 1 data row, got %d", len(result.Data))
		}
		if result.Data[0]["foo"] != "bar" {
			t.Errorf("unexpected foo: %q", result.Data[0]["foo"])
		}
	case <-time.After(time.Second):
		t.Error("not resolved")
	}
}

func TestHandlePacket_VoiceMetadata(t *testing.T) {
	c := newTestClient(t)
	received := make(chan VoiceDataEvent, 2)
	c.OnVoiceData(func(evt VoiceDataEvent) { received <- evt })

	for _, test := range []struct {
		name       string
		packetType transport.PacketType
		isWhisper  bool
	}{
		{name: "channel voice", packetType: transport.PacketTypeVoice},
		{name: "whisper", packetType: transport.PacketTypeVoiceWhisper, isWhisper: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			c.handlePacket(&transport.Packet{
				TypeFlagged: byte(test.packetType),
				ID:          0x1234,
				Data:        []byte{0xab, 0xcd, 0x01, 0x02, 4, 0xaa, 0xbb},
			})

			select {
			case evt := <-received:
				if evt.ClientID != 0x0102 || evt.Codec != 4 {
					t.Errorf("unexpected voice source: clientID=%d codec=%d", evt.ClientID, evt.Codec)
				}
				if evt.Sequence != 0xabcd {
					t.Errorf("sequence = %#x, want payload packet ID %#x", evt.Sequence, uint16(0xabcd))
				}
				if evt.IsWhisper != test.isWhisper {
					t.Errorf("IsWhisper = %t, want %t", evt.IsWhisper, test.isWhisper)
				}
				if string(evt.Data) != string([]byte{0xaa, 0xbb}) {
					t.Errorf("opus data = %x, want aabb", evt.Data)
				}
			case <-time.After(time.Second):
				t.Fatal("voice event not received")
			}
		})
	}
}

func TestHandlePacket_VoiceSequencesArePerSpeaker(t *testing.T) {
	c := newTestClient(t)
	received := make(chan VoiceDataEvent, 4)
	c.OnVoiceData(func(evt VoiceDataEvent) { received <- evt })

	packets := []*transport.Packet{
		{TypeFlagged: byte(transport.PacketTypeVoice), ID: 700, Data: []byte{0, 100, 0, 1, 4, 0xa0}},
		{TypeFlagged: byte(transport.PacketTypeVoice), ID: 701, Data: []byte{1, 244, 0, 2, 4, 0xb0}},
		{TypeFlagged: byte(transport.PacketTypeVoice), ID: 702, Data: []byte{0, 101, 0, 1, 4, 0xa1}},
		{TypeFlagged: byte(transport.PacketTypeVoice), ID: 703, Data: []byte{1, 245, 0, 2, 4, 0xb1}},
	}
	for _, packet := range packets {
		c.handlePacket(packet)
	}

	sequences := map[uint16][]uint16{}
	for range packets {
		select {
		case evt := <-received:
			sequences[evt.ClientID] = append(sequences[evt.ClientID], evt.Sequence)
		case <-time.After(time.Second):
			t.Fatal("voice event not received")
		}
	}

	if got := sequences[1]; len(got) != 2 || got[0] != 100 || got[1] != 101 {
		t.Errorf("client 1 sequences = %v, want [100 101]", got)
	}
	if got := sequences[2]; len(got) != 2 || got[0] != 500 || got[1] != 501 {
		t.Errorf("client 2 sequences = %v, want [500 501]", got)
	}
}

func TestExecCommandWithResponse_Success(t *testing.T) {
	c := newTestClient(t)

	go func() {
		time.Sleep(20 * time.Millisecond)
		// The command sent by ExecCommandWithResponse contains "return_code=N".
		// Simulate server data row then ok error line with matching return_code.
		c.handleCommandLines("rowdata somekey=someval\nerror id=0 msg=ok return_code=1")
	}()

	data, err := c.ExecCommandWithResponse("dummycmd", 2*time.Second)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(data) != 1 || data[0]["somekey"] != "someval" {
		t.Errorf("unexpected data: %v", data)
	}
}

func TestExecCommandWithResponse_Timeout(t *testing.T) {
	c := newTestClient(t)

	start := time.Now()
	_, err := c.ExecCommandWithResponse("dummycmd", 50*time.Millisecond)
	elapsed := time.Since(start)

	if err == nil {
		t.Error("expected timeout error")
	}
	if !strings.Contains(err.Error(), "timeout") {
		t.Errorf("expected 'timeout' in error, got %v", err)
	}
	if elapsed < 40*time.Millisecond || elapsed > 500*time.Millisecond {
		t.Errorf("unexpected elapsed time: %v", elapsed)
	}
}

func TestExecCommandWithResponse_ServerError(t *testing.T) {
	c := newTestClient(t)

	go func() {
		time.Sleep(20 * time.Millisecond)
		c.handleCommandLines("error id=512 msg=invalid_size return_code=1")
	}()

	_, err := c.ExecCommandWithResponse("dummycmd", 2*time.Second)
	if err == nil {
		t.Error("expected error from server")
	}
	if !strings.Contains(err.Error(), "invalid_size") {
		t.Errorf("unexpected error message: %v", err)
	}
}

func mustUint32Str(rc uint32) string {
	return commands.ParseCommand("x return_code=" + itoa(rc)).Params["return_code"]
}

func itoa(n uint32) string {
	if n == 0 {
		return "0"
	}
	buf := make([]byte, 0, 10)
	for n > 0 {
		buf = append([]byte{byte('0' + n%10)}, buf...)
		n /= 10
	}

	return string(buf)
}
