package mqtt

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"net"
	"strings"
	"sync"
	"testing"
	"time"
)

// ---- a broker just real enough to test the client against ---------------------

// brokerConn is the server side of one connection, speaking the same wire codec
// the client does.
type brokerConn struct {
	conn net.Conn
	br   *bufio.Reader
}

func (b *brokerConn) read() (packet, error) { return readPacket(b.br) }

func (b *brokerConn) send(typ, flags byte, body []byte) error {
	_, err := b.conn.Write(buildPacket(typ, flags, body))
	return err
}

// accept reads the CONNECT and answers with an accepting CONNACK, returning the
// CONNECT body so a test can assert on what the client sent.
func (b *brokerConn) accept(t *testing.T) []byte {
	t.Helper()
	pkt, err := b.read()
	if err != nil {
		t.Fatalf("reading CONNECT: %v", err)
	}
	if pkt.typ != pktConnect {
		t.Fatalf("first packet type = %d, want CONNECT(%d)", pkt.typ, pktConnect)
	}
	if err := b.send(pktConnack, 0, []byte{0, 0}); err != nil {
		t.Fatalf("sending CONNACK: %v", err)
	}
	return pkt.body
}

// publish pushes a PUBLISH to the client at the given QoS.
func (b *brokerConn) publish(topic string, payload []byte, qos byte, id uint16, retain bool) error {
	body := encodeString(nil, topic)
	if qos > 0 {
		body = encodeUint16(body, id)
	}
	body = append(body, payload...)
	flags := qos << 1
	if retain {
		flags |= 0x01
	}
	return b.send(pktPublish, flags, body)
}

// startBroker listens on a loopback port and hands the first connection to
// handle. It returns the address to dial.
func startBroker(t *testing.T, handle func(b *brokerConn)) string {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	var wg sync.WaitGroup
	// Close the listener before waiting: cleanups run last-registered first, so
	// registering both together keeps the order right.
	t.Cleanup(func() { _ = ln.Close(); wg.Wait() })
	wg.Add(1)
	go func() {
		defer wg.Done()
		conn, err := ln.Accept()
		if err != nil {
			return
		}
		defer conn.Close()
		handle(&brokerConn{conn: conn, br: bufio.NewReader(conn)})
	}()
	return ln.Addr().String()
}

func dial(t *testing.T, addr string, opt Options) *Client {
	t.Helper()
	opt.Address = addr
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	c, err := Connect(ctx, opt)
	if err != nil {
		t.Fatalf("Connect: %v", err)
	}
	t.Cleanup(func() { _ = c.Close() })
	return c
}

// ---- wire codec --------------------------------------------------------------

func TestRemainingLengthRoundTrip(t *testing.T) {
	// The boundaries of each varint width, per the MQTT spec's own table.
	for _, n := range []int{0, 1, 127, 128, 16383, 16384, 2097151, 2097152, maxRemainingLength} {
		encoded := encodeRemainingLength(nil, n)
		got, err := readRemainingLength(bufio.NewReader(strings.NewReader(string(encoded))))
		if err != nil {
			t.Fatalf("readRemainingLength(%d): %v", n, err)
		}
		if got != n {
			t.Errorf("round trip %d -> %d", n, got)
		}
	}
}

func TestRemainingLengthRejectsOverlongVarint(t *testing.T) {
	// Five continuation bytes is malformed; the spec caps the field at four.
	br := bufio.NewReader(strings.NewReader("\xff\xff\xff\xff\xff"))
	if _, err := readRemainingLength(br); err == nil {
		t.Fatal("expected an error for a five-byte remaining length")
	}
}

func TestReadString(t *testing.T) {
	b := encodeString(nil, "/greencell/broadcast")
	b = append(b, 'x', 'y')
	s, rest, err := readString(b)
	if err != nil {
		t.Fatalf("readString: %v", err)
	}
	if s != "/greencell/broadcast" {
		t.Errorf("string = %q", s)
	}
	if string(rest) != "xy" {
		t.Errorf("rest = %q, want xy", rest)
	}

	if _, _, err := readString([]byte{0x00, 0x05, 'a'}); err == nil {
		t.Error("expected an error for a truncated string")
	}
	if _, _, err := readString([]byte{0x00}); err == nil {
		t.Error("expected an error for a truncated length")
	}
}

func TestConnackError(t *testing.T) {
	if err := connackError(0); err != nil {
		t.Errorf("code 0 should be accepted, got %v", err)
	}
	if err := connackError(4); err == nil || !strings.Contains(err.Error(), "username or password") {
		t.Errorf("code 4 = %v, want a credentials error", err)
	}
	if err := connackError(9); err == nil {
		t.Error("an unknown code should still be an error")
	}
}

// ---- handshake ---------------------------------------------------------------

func TestConnectSendsCredentialsAndKeepalive(t *testing.T) {
	var got []byte
	done := make(chan struct{})
	addr := startBroker(t, func(b *brokerConn) {
		got = b.accept(t)
		close(done)
		// Hold the connection open so the client stays up for the assertions.
		_, _ = b.read()
	})

	dial(t, addr, Options{ClientID: "dv-test", Username: "user", Password: "pass", Keepalive: 42 * time.Second})
	<-done

	name, rest, err := readString(got)
	if err != nil || name != protocolName {
		t.Fatalf("protocol name = %q (err %v), want MQTT", name, err)
	}
	if rest[0] != protocolLevel {
		t.Errorf("protocol level = %d, want %d", rest[0], protocolLevel)
	}
	flags := rest[1]
	if flags&0x02 == 0 {
		t.Error("clean session flag should be set")
	}
	if flags&0x80 == 0 || flags&0x40 == 0 {
		t.Errorf("username+password flags should be set, got %#x", flags)
	}
	if ka := binary.BigEndian.Uint16(rest[2:]); ka != 42 {
		t.Errorf("keepalive = %d, want 42", ka)
	}

	id, rest, err := readString(rest[4:])
	if err != nil || id != "dv-test" {
		t.Fatalf("client id = %q (err %v)", id, err)
	}
	user, rest, err := readString(rest)
	if err != nil || user != "user" {
		t.Fatalf("username = %q (err %v)", user, err)
	}
	pass, _, err := readString(rest)
	if err != nil || pass != "pass" {
		t.Fatalf("password = %q (err %v)", pass, err)
	}
}

func TestConnectOmitsCredentialsWhenUnset(t *testing.T) {
	var got []byte
	done := make(chan struct{})
	addr := startBroker(t, func(b *brokerConn) {
		got = b.accept(t)
		close(done)
		_, _ = b.read()
	})

	dial(t, addr, Options{ClientID: "dv-test"})
	<-done

	_, rest, err := readString(got)
	if err != nil {
		t.Fatalf("readString: %v", err)
	}
	if flags := rest[1]; flags&0xC0 != 0 {
		t.Errorf("credential flags should be clear on an anonymous connect, got %#x", flags)
	}
}

func TestConnectRefused(t *testing.T) {
	addr := startBroker(t, func(b *brokerConn) {
		if _, err := b.read(); err != nil {
			return
		}
		_ = b.send(pktConnack, 0, []byte{0, 5}) // not authorized
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, err := Connect(ctx, Options{Address: addr})
	if err == nil {
		t.Fatal("expected the refusal to surface as an error")
	}
	if !strings.Contains(err.Error(), "not authorized") {
		t.Errorf("error = %v, want the CONNACK reason", err)
	}
}

func TestConnectRejectsNonConnack(t *testing.T) {
	addr := startBroker(t, func(b *brokerConn) {
		if _, err := b.read(); err != nil {
			return
		}
		_ = b.send(pktPingresp, 0, nil)
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if _, err := Connect(ctx, Options{Address: addr}); err == nil {
		t.Fatal("expected an error when the broker answers something other than CONNACK")
	}
}

func TestConnectUnreachableBroker(t *testing.T) {
	// Port 0 never accepts, so this fails at dial rather than at handshake.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, err := Connect(ctx, Options{Address: "127.0.0.1:0", ConnectTimeout: time.Second})
	if err == nil {
		t.Fatal("expected a dial error")
	}
	if !strings.Contains(err.Error(), "cannot reach broker") {
		t.Errorf("error = %v, want a broker-unreachable message", err)
	}
}

func TestConnectRequiresAddress(t *testing.T) {
	if _, err := Connect(context.Background(), Options{}); err == nil {
		t.Fatal("expected an error when no broker address is configured")
	}
}

// ---- subscribe / publish -----------------------------------------------------

func TestSubscribeAndDeliver(t *testing.T) {
	var topics []string
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		pkt, err := b.read()
		if err != nil {
			return
		}
		if pkt.typ != pktSubscribe {
			t.Errorf("packet type = %d, want SUBSCRIBE(%d)", pkt.typ, pktSubscribe)
			return
		}
		if pkt.flags != 0x02 {
			t.Errorf("SUBSCRIBE flags = %#x, want 0x02", pkt.flags)
		}
		id := binary.BigEndian.Uint16(pkt.body)
		rest := pkt.body[2:]
		for len(rest) > 0 {
			var topic string
			topic, rest, err = readString(rest)
			if err != nil {
				return
			}
			topics = append(topics, topic)
			rest = rest[1:] // requested QoS
		}
		_ = b.send(pktSuback, 0, append(encodeUint16(nil, id), 0, 0))
		_ = b.publish("/greencell/evse/SN1/power", []byte(`{"momentary":7360}`), 0, 0, true)
		_, _ = b.read()
	})

	c := dial(t, addr, Options{})
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := c.Subscribe(ctx, "/greencell/broadcast/device", "/greencell/evse/SN1/power"); err != nil {
		t.Fatalf("Subscribe: %v", err)
	}
	if len(topics) != 2 || topics[0] != "/greencell/broadcast/device" {
		t.Errorf("broker saw topics %v", topics)
	}

	select {
	case msg := <-c.Messages():
		if msg.Topic != "/greencell/evse/SN1/power" {
			t.Errorf("topic = %q", msg.Topic)
		}
		if string(msg.Payload) != `{"momentary":7360}` {
			t.Errorf("payload = %q", msg.Payload)
		}
		if !msg.Retain {
			t.Error("retain flag should have been carried through")
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the delivery")
	}
}

func TestSubscribeRefused(t *testing.T) {
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		pkt, err := b.read()
		if err != nil {
			return
		}
		id := binary.BigEndian.Uint16(pkt.body)
		_ = b.send(pktSuback, 0, append(encodeUint16(nil, id), 0x80))
		_, _ = b.read()
	})

	c := dial(t, addr, Options{})
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	err := c.Subscribe(ctx, "/greencell/broadcast/device")
	if err == nil || !strings.Contains(err.Error(), "refused subscription") {
		t.Fatalf("Subscribe error = %v, want a refusal", err)
	}
}

func TestSubscribeNoTopicsIsNoOp(t *testing.T) {
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		_, _ = b.read()
	})
	c := dial(t, addr, Options{})
	if err := c.Subscribe(context.Background()); err != nil {
		t.Fatalf("Subscribe with no topics = %v, want nil", err)
	}
}

func TestPublish(t *testing.T) {
	type sent struct {
		topic   string
		payload string
	}
	got := make(chan sent, 1)
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		pkt, err := b.read()
		if err != nil {
			return
		}
		if pkt.typ != pktPublish {
			t.Errorf("packet type = %d, want PUBLISH(%d)", pkt.typ, pktPublish)
			return
		}
		if qos := (pkt.flags >> 1) & 0x03; qos != 0 {
			t.Errorf("published QoS = %d, want 0", qos)
		}
		topic, rest, err := readString(pkt.body)
		if err != nil {
			return
		}
		got <- sent{topic, string(rest)}
		_, _ = b.read()
	})

	c := dial(t, addr, Options{})
	if err := c.Publish(context.Background(), "/greencell/broadcast", []byte(`{"name":"BROADCAST"}`)); err != nil {
		t.Fatalf("Publish: %v", err)
	}
	select {
	case s := <-got:
		if s.topic != "/greencell/broadcast" || s.payload != `{"name":"BROADCAST"}` {
			t.Errorf("broker received %+v", s)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the publish")
	}
}

func TestPublishRejectsEmptyTopic(t *testing.T) {
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		_, _ = b.read()
	})
	c := dial(t, addr, Options{})
	if err := c.Publish(context.Background(), "", nil); err == nil {
		t.Fatal("expected an error for an empty topic")
	}
}

// TestInboundQoS1IsAcknowledged covers the case the client does not choose: the
// broker may upgrade a delivery to QoS 1, and an unacknowledged one is redelivered
// forever.
func TestInboundQoS1IsAcknowledged(t *testing.T) {
	acked := make(chan uint16, 1)
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		_ = b.publish("/greencell/evse/SN1/status", []byte(`{"state":"CHARGING"}`), 1, 77, false)
		for {
			pkt, err := b.read()
			if err != nil {
				return
			}
			if pkt.typ == pktPuback && len(pkt.body) >= 2 {
				acked <- binary.BigEndian.Uint16(pkt.body)
				return
			}
		}
	})

	c := dial(t, addr, Options{})
	select {
	case msg := <-c.Messages():
		if string(msg.Payload) != `{"state":"CHARGING"}` {
			t.Errorf("payload = %q — the packet identifier should be stripped", msg.Payload)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the QoS 1 delivery")
	}
	select {
	case id := <-acked:
		if id != 77 {
			t.Errorf("PUBACK id = %d, want 77", id)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the PUBACK")
	}
}

// TestInboundQoS2Completes walks the four-way exchange the broker drives.
func TestInboundQoS2Completes(t *testing.T) {
	done := make(chan byte, 2)
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		_ = b.publish("/greencell/evse/SN1/status", []byte(`{"state":"IDLE"}`), 2, 88, false)
		for {
			pkt, err := b.read()
			if err != nil {
				return
			}
			switch pkt.typ {
			case pktPubrec:
				done <- pktPubrec
				_ = b.send(pktPubrel, 0x02, encodeUint16(nil, 88))
			case pktPubcomp:
				done <- pktPubcomp
				return
			}
		}
	})

	c := dial(t, addr, Options{})
	select {
	case <-c.Messages():
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the QoS 2 delivery")
	}
	for _, want := range []byte{pktPubrec, pktPubcomp} {
		select {
		case got := <-done:
			if got != want {
				t.Fatalf("exchange step = %d, want %d", got, want)
			}
		case <-time.After(5 * time.Second):
			t.Fatalf("timed out waiting for step %d", want)
		}
	}
}

// ---- teardown ----------------------------------------------------------------

func TestMessagesCloseAndErrOnBrokerDisconnect(t *testing.T) {
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		_ = b.conn.Close()
	})

	c := dial(t, addr, Options{})
	select {
	case _, ok := <-c.Messages():
		if ok {
			t.Fatal("expected the message channel to close, not deliver")
		}
	case <-time.After(5 * time.Second):
		t.Fatal("timed out waiting for the channel to close")
	}
	if c.Err() == nil {
		t.Error("Err should report why the connection ended")
	}
}

func TestCloseIsIdempotentAndSilencesErr(t *testing.T) {
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		for {
			if _, err := b.read(); err != nil {
				return
			}
		}
	})

	c := dial(t, addr, Options{})
	if err := c.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}
	if err := c.Close(); err != nil {
		t.Fatalf("second Close: %v", err)
	}
	if err := c.Err(); err != nil {
		t.Errorf("a clean Close should leave Err nil, got %v", err)
	}
	if err := c.Publish(context.Background(), "/x", nil); !errors.Is(err, ErrClosed) {
		t.Errorf("Publish after Close = %v, want ErrClosed", err)
	}
}

func TestSubscribeHonoursContextCancellation(t *testing.T) {
	// A broker that never answers the SUBSCRIBE: the caller's deadline is what
	// has to end the wait.
	addr := startBroker(t, func(b *brokerConn) {
		b.accept(t)
		for {
			if _, err := b.read(); err != nil {
				return
			}
		}
	})

	c := dial(t, addr, Options{})
	ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
	defer cancel()
	if err := c.Subscribe(ctx, "/greencell/broadcast/device"); !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("Subscribe = %v, want a deadline error", err)
	}
}
