defmodule Microwaveprop.Pskr.MqttTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Pskr.Mqtt

  describe "encode_varint/1 + decode_varint/1" do
    # Boundary values from the MQTT 3.1.1 spec, table 2.4.
    @cases [
      {0, 1},
      {127, 1},
      {128, 2},
      {16_383, 2},
      {16_384, 3},
      {2_097_151, 3},
      {2_097_152, 4},
      {268_435_455, 4}
    ]

    test "round-trip encodes to the expected number of bytes" do
      for {n, len} <- @cases do
        encoded = Mqtt.encode_varint(n)
        assert byte_size(encoded) == len, "len mismatch for #{n}"
        assert {:ok, ^n, <<>>} = Mqtt.decode_varint(encoded)
      end
    end

    test "decode returns :incomplete on truncated input" do
      # 0x80 has the continuation bit set but no following byte.
      assert :incomplete == Mqtt.decode_varint(<<0x80>>)
    end

    test "decode returns the unconsumed tail" do
      assert {:ok, 200, "tail"} = Mqtt.decode_varint(Mqtt.encode_varint(200) <> "tail")
    end
  end

  describe "connect/2" do
    test "produces a CONNECT packet for the given client id and keepalive" do
      packet = Mqtt.connect("test-client", 60)

      # Fixed header: type=1, flags=0 → 0x10
      assert <<0x10, _rest::binary>> = packet

      # Body should contain the protocol header verbatim.
      assert :binary.match(packet, "MQTT") != :nomatch
      assert :binary.match(packet, "test-client") != :nomatch
    end
  end

  describe "subscribe/2" do
    test "encodes packet id and each topic with its QoS" do
      packet = Mqtt.subscribe(42, [{"foo/bar", 0}, {"baz", 1}])

      # Type=8, reserved bits=0010 → 0x82
      assert <<0x82, _rest::binary>> = packet
      assert :binary.match(packet, "foo/bar") != :nomatch
      assert :binary.match(packet, "baz") != :nomatch
      # Packet ID 42 lands in the variable header right after the
      # remaining-length byte (which is < 128 → single byte).
      assert <<0x82, _len, 0::8, 42::8, _::binary>> = packet
    end
  end

  describe "parse/1" do
    test "decodes CONNACK with session_present + return_code" do
      assert {:ok, {:connack, 0, false}, ""} = Mqtt.parse(<<0x20, 0x02, 0x00, 0x00>>)
      assert {:ok, {:connack, 5, true}, ""} = Mqtt.parse(<<0x20, 0x02, 0x01, 0x05>>)
    end

    test "decodes a QoS 0 PUBLISH" do
      topic = "pskr/filter/v2/2m/FT8"
      payload = ~s({"sq":1})
      body = <<byte_size(topic)::16, topic::binary, payload::binary>>
      packet = <<0x30, byte_size(body)>> <> body

      assert {:ok, {:publish, ^topic, ^payload}, ""} = Mqtt.parse(packet)
    end

    test "decodes SUBACK with per-topic QoS results" do
      assert {:ok, {:suback, 7, [0, 1, 0x80]}, ""} = Mqtt.parse(<<0x90, 0x05, 0::8, 7::8, 0, 1, 0x80>>)
    end

    test "decodes PINGRESP" do
      assert {:ok, :pingresp, ""} = Mqtt.parse(<<0xD0, 0x00>>)
    end

    test "returns :incomplete on a short remaining-length read" do
      # Just the type byte, no length yet.
      assert {:incomplete, <<0x30>>} = Mqtt.parse(<<0x30>>)
    end

    test "returns :incomplete when body is shorter than declared length" do
      # Says length=10 but only gives 3 bytes.
      assert {:incomplete, _} = Mqtt.parse(<<0x30, 10, "abc">>)
    end

    test "returns trailing bytes after a complete packet" do
      first = <<0xD0, 0x00>>
      tail = <<0xC0, 0x00>>
      assert {:ok, :pingresp, ^tail} = Mqtt.parse(first <> tail)
    end

    test "empty buffer returns :incomplete" do
      assert {:incomplete, <<>>} = Mqtt.parse(<<>>)
    end

    test "rejects PUBLISH with QoS > 0" do
      # PUBLISH with QoS 1 (type=3, flags=2 → 0x32)
      assert {:error, {:unsupported_qos, 1}} = Mqtt.parse(<<0x32, 0x00>>)
    end

    test "returns error for unknown packet type" do
      assert {:error, {:unhandled_packet_type, _}} = Mqtt.parse(<<0x80, 0x00>>)
    end
  end

  describe "pingreq/0" do
    test "produces 0xC0 0x00" do
      assert Mqtt.pingreq() == <<0xC0, 0x00>>
    end
  end

  describe "disconnect/0" do
    test "produces 0xE0 0x00" do
      assert Mqtt.disconnect() == <<0xE0, 0x00>>
    end
  end

  describe "decode_varint error paths" do
    test "returns error on varint longer than 4 bytes" do
      # 5 continuation bytes — spec says max 4
      assert {:error, :varint_too_long} = Mqtt.decode_varint(<<0x80, 0x80, 0x80, 0x80, 0x00>>)
    end

    test "returns incomplete on empty buffer" do
      assert :incomplete == Mqtt.decode_varint(<<>>)
    end
  end
end
