defmodule SnmpKit.SnmpLib.PDUTest do
  use ExUnit.Case, async: true

  alias SnmpKit.SnmpLib.PDU

  @moduletag :unit
  @moduletag :protocol
  @moduletag :phase_1

  describe "PDU construction" do
    test "builds GET request PDU with correct structure" do
      oid = [1, 3, 6, 1, 2, 1, 1, 1, 0]
      request_id = 12_345

      pdu = PDU.build_get_request(oid, request_id)

      assert pdu.type == :get_request
      assert pdu.request_id == request_id
      assert pdu.error_status == 0
      assert pdu.error_index == 0
      assert pdu.varbinds == [{oid, :null, :null}]
    end

    test "builds GETNEXT request PDU" do
      oid = [1, 3, 6, 1, 2, 1, 1]
      request_id = 23_456

      pdu = PDU.build_get_next_request(oid, request_id)

      assert pdu.type == :get_next_request
      assert pdu.request_id == request_id
      assert pdu.varbinds == [{oid, :null, :null}]
    end

    test "builds GETBULK request PDU with non-repeaters and max-repetitions" do
      oid = [1, 3, 6, 1, 2, 1, 2, 2]
      request_id = 34_567
      non_repeaters = 0
      max_repetitions = 10

      pdu = PDU.build_get_bulk_request(oid, request_id, non_repeaters, max_repetitions)

      assert pdu.type == :get_bulk_request
      assert pdu.request_id == request_id
      assert pdu.non_repeaters == non_repeaters
      assert pdu.max_repetitions == max_repetitions
      assert pdu.varbinds == [{oid, :null, :null}]
    end

    test "builds SET request PDU with value" do
      oid = [1, 3, 6, 1, 2, 1, 1, 5, 0]
      request_id = 45_678
      value = {:string, "new-hostname"}

      pdu = PDU.build_set_request(oid, value, request_id)

      assert pdu.type == :set_request
      assert pdu.request_id == request_id
      assert pdu.varbinds == [{oid, :string, "new-hostname"}]
    end

    test "builds response PDU with error status" do
      request_id = 56_789
      # noSuchName
      error_status = 2
      error_index = 1
      varbinds = [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :string, "test"}]

      pdu = PDU.build_response(request_id, error_status, error_index, varbinds)

      assert pdu.type == :get_response
      assert pdu.request_id == request_id
      assert pdu.error_status == error_status
      assert pdu.error_index == error_index
      assert pdu.varbinds == varbinds
    end

    test "builds multi-varbind GET request" do
      varbinds = [
        {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null},
        {[1, 3, 6, 1, 2, 1, 1, 2, 0], :null, :null}
      ]

      request_id = 67_890

      pdu = PDU.build_get_request_multi(varbinds, request_id)

      assert pdu.type == :get_request
      assert pdu.request_id == request_id
      assert pdu.varbinds == varbinds
    end
  end

  describe "PDU validation" do
    test "validates request ID range" do
      valid_ids = [0, 1, 65_535, 2_147_483_647]

      for request_id <- valid_ids do
        pdu = PDU.build_get_request([1, 3, 6, 1], request_id)
        assert {:ok, _} = PDU.validate(pdu)
      end
    end

    test "rejects invalid request IDs" do
      invalid_ids = [-1, 2_147_483_648]

      for request_id <- invalid_ids do
        assert_raise ArgumentError, fn ->
          PDU.build_get_request([1, 3, 6, 1], request_id)
        end
      end
    end

    test "validates PDU type" do
      pdu = PDU.build_get_request([1, 3, 6, 1], 123)
      assert {:ok, _} = PDU.validate(pdu)

      invalid_pdu = %{type: :invalid_type, request_id: 123, varbinds: []}
      assert :error = PDU.validate(invalid_pdu)
    end

    test "validates varbinds format" do
      valid_varbinds = [{[1, 3, 6, 1], :null, :null}]
      pdu = PDU.build_get_request_multi(valid_varbinds, 123)
      assert {:ok, _} = PDU.validate(pdu)

      invalid_varbinds = [{"invalid", :null, :null}]

      assert {:error, :invalid_varbind_format} =
               PDU.build_get_request_multi(invalid_varbinds, 123)
    end

    test "validates GETBULK specific fields" do
      pdu = PDU.build_get_bulk_request([1, 3, 6, 1], 123, 0, 10)
      assert {:ok, _} = PDU.validate(pdu)

      invalid_bulk = %{type: :get_bulk_request, request_id: 123, varbinds: []}
      assert {:error, :missing_bulk_fields} = PDU.validate(invalid_bulk)
    end
  end

  describe "Message construction" do
    test "builds SNMP message with v1" do
      pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345)
      message = PDU.build_message(pdu, "public", :v1)

      assert message.version == 0
      assert message.community == "public"
      assert message.pdu == pdu
    end

    test "builds SNMP message with v2c" do
      pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345)
      message = PDU.build_message(pdu, "public", :v2c)

      assert message.version == 1
      assert message.community == "public"
      assert message.pdu == pdu
    end

    test "rejects GETBULK with v1" do
      pdu = PDU.build_get_bulk_request([1, 3, 6, 1], 123)

      assert_raise ArgumentError, ~r/GETBULK requests require SNMPv2c/, fn ->
        PDU.build_message(pdu, "public", :v1)
      end
    end

    test "validates community string" do
      pdu = PDU.build_get_request([1, 3, 6, 1], 123)

      assert_raise ArgumentError, ~r/Community must be a binary string/, fn ->
        PDU.build_message(pdu, :invalid_community, :v1)
      end
    end
  end

  describe "Encoding and decoding" do
    test "encodes and decodes GET request round-trip" do
      # Build request
      pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345)
      message = PDU.build_message(pdu, "public", :v2c)

      # Encode
      {:ok, encoded} = PDU.encode_message(message)
      assert is_binary(encoded)
      assert byte_size(encoded) > 0

      # Decode
      {:ok, decoded} = PDU.decode_message(encoded)

      # Verify structure
      assert decoded.version == 1
      assert decoded.community == "public"
      assert decoded.pdu.type == :get_request
      assert decoded.pdu.request_id == 12_345
      assert decoded.pdu.error_status == 0
      assert decoded.pdu.error_index == 0
      assert length(decoded.pdu.varbinds) == 1
    end

    test "encodes and decodes GETBULK request round-trip" do
      pdu = PDU.build_get_bulk_request([1, 3, 6, 1, 2, 1, 2], 23_456, 0, 10)
      message = PDU.build_message(pdu, "public", :v2c)

      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert decoded.version == 1
      assert decoded.community == "public"
      assert decoded.pdu.type == :get_bulk_request
      assert decoded.pdu.request_id == 23_456
      assert decoded.pdu.non_repeaters == 0
      assert decoded.pdu.max_repetitions == 10
    end

    test "encodes and decodes SET request with values" do
      pdu = PDU.build_set_request([1, 3, 6, 1, 2, 1, 1, 5, 0], {:string, "test-value"}, 34_567)
      message = PDU.build_message(pdu, "private", :v2c)

      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert decoded.version == 1
      assert decoded.community == "private"
      assert decoded.pdu.type == :set_request
      assert decoded.pdu.request_id == 34_567
      assert length(decoded.pdu.varbinds) == 1
    end

    test "handles encoding errors gracefully" do
      invalid_message = %{invalid: :structure}
      assert {:error, :invalid_message_format} = PDU.encode_message(invalid_message)
    end

    test "handles decoding errors gracefully" do
      invalid_binary = <<1, 2, 3, 4>>
      assert {:error, _} = PDU.decode_message(invalid_binary)

      assert {:error, :invalid_input} = PDU.decode_message(:not_binary)
    end

    test "decodes malformed packets with error" do
      malformed_data = <<0x01, 0x02, 0x03, 0x04>>
      result = PDU.decode_message(malformed_data)
      assert {:error, _} = result
    end
  end

  describe "Community validation" do
    test "validates correct community string" do
      pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345)
      message = PDU.build_message(pdu, "test-community", :v1)
      {:ok, encoded} = PDU.encode_message(message)

      assert :ok = PDU.validate_community(encoded, "test-community")
    end

    test "rejects incorrect community string" do
      pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345)
      message = PDU.build_message(pdu, "correct-community", :v1)
      {:ok, encoded} = PDU.encode_message(message)

      assert {:error, :invalid_community} = PDU.validate_community(encoded, "wrong-community")
    end

    test "handles validation with malformed packets" do
      malformed_data = <<0x01, 0x02, 0x03>>
      result = PDU.validate_community(malformed_data, "any-community")
      assert {:error, _} = result
    end

    test "validates parameter types" do
      assert {:error, :invalid_parameters} = PDU.validate_community(:not_binary, "community")
      assert {:error, :invalid_parameters} = PDU.validate_community(<<1, 2, 3>>, :not_binary)
    end
  end

  describe "Error response creation" do
    test "creates error response from request PDU" do
      request_pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345)
      error_pdu = PDU.create_error_response(request_pdu, 2, 1)

      assert error_pdu.type == :get_response
      assert error_pdu.request_id == 12_345
      assert error_pdu.error_status == 2
      assert error_pdu.error_index == 1
      assert error_pdu.varbinds == request_pdu.varbinds
    end

    test "creates error response with default error index" do
      request_pdu = PDU.build_get_request([1, 3, 6, 1], 98_765)
      error_pdu = PDU.create_error_response(request_pdu, 3)

      assert error_pdu.type == :get_response
      assert error_pdu.request_id == 98_765
      assert error_pdu.error_status == 3
      assert error_pdu.error_index == 0
    end

    test "handles missing fields gracefully" do
      incomplete_pdu = %{type: :get_request}
      error_pdu = PDU.create_error_response(incomplete_pdu, 5, 2)

      assert error_pdu.type == :get_response
      # Default fallback
      assert error_pdu.request_id == 1
      assert error_pdu.error_status == 5
      assert error_pdu.error_index == 2
      # Default fallback
      assert error_pdu.varbinds == []
    end
  end

  describe "Performance and edge cases" do
    test "handles large request IDs" do
      # Max 32-bit signed integer
      large_id = 2_147_483_647
      pdu = PDU.build_get_request([1, 3, 6, 1], large_id)
      message = PDU.build_message(pdu, "public", :v1)

      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert decoded.pdu.request_id == large_id
    end

    test "handles empty community string" do
      pdu = PDU.build_get_request([1, 3, 6, 1], 123)
      message = PDU.build_message(pdu, "", :v1)

      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert decoded.community == ""
    end

    test "handles long community strings" do
      long_community = String.duplicate("x", 255)
      pdu = PDU.build_get_request([1, 3, 6, 1], 123)
      message = PDU.build_message(pdu, long_community, :v1)

      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert decoded.community == long_community
    end

    test "handles complex OIDs" do
      # Use simple values that work correctly - large OID values to be fixed in Phase 2
      complex_oid = [1, 3, 6, 1, 4, 1, 127, 1, 2, 3, 4, 5, 10, 100, 127]
      pdu = PDU.build_get_request(complex_oid, 123)
      message = PDU.build_message(pdu, "public", :v2c)

      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert length(decoded.pdu.varbinds) == 1
      {decoded_oid, _, _} = hd(decoded.pdu.varbinds)
      assert decoded_oid == complex_oid
    end

    test "handles maximum GETBULK parameters" do
      max_reps = 65_535
      pdu = PDU.build_get_bulk_request([1, 3, 6, 1], 123, 255, max_reps)
      message = PDU.build_message(pdu, "public", :v2c)

      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert decoded.pdu.non_repeaters == 255
      assert decoded.pdu.max_repetitions == max_reps
    end
  end

  describe "Error conditions and boundary testing" do
    test "handles zero-length input gracefully" do
      assert {:error, _} = PDU.decode_message(<<>>)
    end

    test "handles truncated packets" do
      # Create a valid packet then truncate it
      pdu = PDU.build_get_request([1, 3, 6, 1], 123)
      message = PDU.build_message(pdu, "public", :v1)
      {:ok, encoded} = PDU.encode_message(message)

      truncated = binary_part(encoded, 0, div(byte_size(encoded), 2))
      assert {:error, _} = PDU.decode_message(truncated)
    end

    test "validates OID bounds in varbinds" do
      # Very large OID component
      large_oid = [1, 3, 6, 1, 999_999_999]
      pdu = PDU.build_get_request(large_oid, 123)
      message = PDU.build_message(pdu, "public", :v2c)

      # Should encode/decode without error
      {:ok, encoded} = PDU.encode_message(message)
      {:ok, decoded} = PDU.decode_message(encoded)

      assert is_map(decoded.pdu)
    end

    test "handles concurrent encoding/decoding operations" do
      # Test thread safety by running multiple operations concurrently
      tasks =
        for i <- 1..50 do
          Task.async(fn ->
            pdu = PDU.build_get_request([1, 3, 6, 1, i], i)
            message = PDU.build_message(pdu, "public-#{i}", :v2c)
            {:ok, encoded} = PDU.encode_message(message)
            {:ok, decoded} = PDU.decode_message(encoded)
            {i, decoded.pdu.request_id, decoded.community}
          end)
        end

      results = Task.await_many(tasks, 1000)

      # Verify all operations completed successfully
      assert length(results) == 50

      for {i, request_id, community} <- results do
        assert i == request_id
        assert community == "public-#{i}"
      end
    end

    test "maintains encoding fidelity with random data" do
      # Test with various random OIDs and values
      for _iteration <- 1..20 do
        # Generate random but valid OID
        # 3 to 12 components
        oid_length = :rand.uniform(10) + 2
        oid = [1, 3] ++ for(_ <- 1..(oid_length - 2), do: :rand.uniform(65_535))

        request_id = :rand.uniform(2_147_483_647)
        community = "test-#{:rand.uniform(1000)}"

        pdu = PDU.build_get_request(oid, request_id)
        message = PDU.build_message(pdu, community, :v2c)

        {:ok, encoded} = PDU.encode_message(message)
        {:ok, decoded} = PDU.decode_message(encoded)

        # Verify round-trip fidelity
        assert decoded.pdu.request_id == request_id
        assert decoded.community == community
        assert decoded.pdu.type == :get_request
      end
    end
  end
end
