defmodule Towerops.Agent.ValidatorTest do
  use ExUnit.Case, async: true

  alias Towerops.Agent.AgentError
  alias Towerops.Agent.AgentHeartbeat
  alias Towerops.Agent.CredentialTestResult
  alias Towerops.Agent.InterfaceStat
  alias Towerops.Agent.Metric
  alias Towerops.Agent.MetricBatch
  alias Towerops.Agent.MikrotikResult
  alias Towerops.Agent.MikrotikSentence
  alias Towerops.Agent.MonitoringCheck
  alias Towerops.Agent.NeighborDiscovery
  alias Towerops.Agent.SensorReading
  alias Towerops.Agent.SnmpResult
  alias Towerops.Agent.Validator

  describe "validate_heartbeat/1" do
    test "validates valid heartbeat" do
      heartbeat = %AgentHeartbeat{
        version: "1.2.3",
        hostname: "agent01.example.com",
        uptime_seconds: 12_345,
        ip_address: "192.168.1.100"
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, validated} = Validator.validate_heartbeat(binary)
      assert validated.version == "1.2.3"
      assert validated.hostname == "agent01.example.com"
    end

    test "validates heartbeat with IPv6 address" do
      heartbeat = %AgentHeartbeat{
        version: "2.0.0",
        hostname: "agent02",
        uptime_seconds: 100,
        ip_address: "2001:db8::1"
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "rejects invalid version format" do
      heartbeat = %AgentHeartbeat{
        version: "not-semver",
        hostname: "agent03",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects invalid hostname" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "invalid..hostname",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects excessive uptime" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "agent04",
        uptime_seconds: 5_000_000_000,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_uptime, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects invalid IP address" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "agent05",
        uptime_seconds: 100,
        ip_address: "999.999.999.999"
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_ip, _}} = Validator.validate_heartbeat(binary)
    end

    test "accepts empty IP address" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "agent06",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "rejects malformed protobuf" do
      invalid_binary = <<0, 1, 2, 3, 4, 5>>
      assert {:error, {:decode_error, _}} = Validator.validate_heartbeat(invalid_binary)
    end
  end

  describe "validate_metric_batch/1" do
    test "validates batch with sensor readings" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 23.5,
        status: "ok",
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:sensor_reading, reading}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, validated} = Validator.validate_metric_batch(binary)
      assert length(validated.metrics) == 1
    end

    test "validates batch with interface stats" do
      stat = %InterfaceStat{
        interface_id: "550e8400-e29b-41d4-a716-446655440001",
        if_in_octets: 1000,
        if_out_octets: 2000,
        if_in_errors: 0,
        if_out_errors: 0,
        if_in_discards: 0,
        if_out_discards: 0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:interface_stat, stat}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "validates batch with neighbor discovery" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "lldp",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "switch01",
        remote_system_description: "Cisco IOS",
        remote_platform: "C9300",
        remote_port_id: "Gi1/0/1",
        remote_port_description: "uplink",
        remote_address: "192.168.1.1",
        remote_capabilities: ["bridge", "router"],
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:neighbor_discovery, discovery}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "validates batch with monitoring check" do
      check = %MonitoringCheck{
        device_id: "550e8400-e29b-41d4-a716-446655440003",
        status: "success",
        response_time_ms: 12.5,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:monitoring_check, check}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "rejects invalid sensor UUID" do
      reading = %SensorReading{
        sensor_id: "not-a-uuid",
        value: 23.5,
        status: "ok",
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:sensor_reading, reading}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_sensor_id, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects invalid status" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 23.5,
        status: "invalid_status",
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:sensor_reading, reading}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_status, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects negative counters" do
      stat = %InterfaceStat{
        interface_id: "550e8400-e29b-41d4-a716-446655440001",
        if_in_octets: -1000,
        if_out_octets: 2000,
        if_in_errors: 0,
        if_out_errors: 0,
        if_in_discards: 0,
        if_out_discards: 0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:interface_stat, stat}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects invalid protocol" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "invalid",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "switch01",
        remote_system_description: "",
        remote_platform: "",
        remote_port_id: "",
        remote_port_description: "",
        remote_address: "",
        remote_capabilities: [],
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:neighbor_discovery, discovery}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_protocol, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects excessive response time" do
      check = %MonitoringCheck{
        device_id: "550e8400-e29b-41d4-a716-446655440003",
        status: "success",
        response_time_ms: 99_999.0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:monitoring_check, check}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_response_time, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects future timestamp" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 23.5,
        status: "ok",
        timestamp: 9_999_999_999
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:sensor_reading, reading}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_timestamp, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects too many capabilities" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "lldp",
        remote_chassis_id: "",
        remote_system_name: "",
        remote_system_description: "",
        remote_platform: "",
        remote_port_id: "",
        remote_port_description: "",
        remote_address: "",
        remote_capabilities: Enum.map(1..25, &"cap#{&1}"),
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:neighbor_discovery, discovery}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:too_many_capabilities, _}} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_snmp_result/1" do
    test "validates valid SNMP result" do
      result = %SnmpResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_type: 1,
        oid_values: %{"1.3.6.1.2.1.1.1.0" => "Test Device"},
        timestamp: 1_700_000_000
      }

      binary = SnmpResult.encode(result)
      assert {:ok, validated} = Validator.validate_snmp_result(binary)
      assert validated.device_id == result.device_id
    end

    test "rejects invalid device UUID" do
      result = %SnmpResult{
        device_id: "not-a-uuid",
        job_type: 1,
        oid_values: %{},
        timestamp: 1_700_000_000
      }

      binary = SnmpResult.encode(result)
      assert {:error, {:invalid_device_id, _}} = Validator.validate_snmp_result(binary)
    end

    test "rejects too many OID values" do
      # Create map with more than max allowed OIDs (max is 50_000)
      oid_values = Map.new(1..50_100, fn i -> {"1.3.6.1.#{i}", "value"} end)

      result = %SnmpResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_type: 1,
        oid_values: oid_values,
        timestamp: 1_700_000_000
      }

      binary = SnmpResult.encode(result)
      assert {:error, {:too_many_oids, _}} = Validator.validate_snmp_result(binary)
    end
  end

  describe "validate_agent_error/1" do
    test "validates valid agent error" do
      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        message: "Connection timeout"
      }

      binary = AgentError.encode(error)
      assert {:ok, validated} = Validator.validate_agent_error(binary)
      assert validated.message == "Connection timeout"
    end

    test "rejects excessively long error message" do
      long_message = String.duplicate("error ", 250)

      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        message: long_message
      }

      binary = AgentError.encode(error)
      assert {:error, {:string_too_long, _}} = Validator.validate_agent_error(binary)
    end
  end

  describe "validate_credential_test_result/1" do
    test "validates successful credential test" do
      result = %CredentialTestResult{
        test_id: "550e8400-e29b-41d4-a716-446655440000",
        success: true,
        error_message: "",
        system_description: "Cisco IOS Software",
        timestamp: 1_700_000_000
      }

      binary = CredentialTestResult.encode(result)
      assert {:ok, validated} = Validator.validate_credential_test_result(binary)
      assert validated.success == true
    end

    test "validates failed credential test" do
      result = %CredentialTestResult{
        test_id: "550e8400-e29b-41d4-a716-446655440000",
        success: false,
        error_message: "Authentication failed",
        system_description: "",
        timestamp: 1_700_000_000
      }

      binary = CredentialTestResult.encode(result)
      assert {:ok, validated} = Validator.validate_credential_test_result(binary)
      assert validated.success == false
    end

    test "rejects invalid test ID" do
      result = %CredentialTestResult{
        test_id: "invalid-id",
        success: true,
        error_message: "",
        system_description: "",
        timestamp: 1_700_000_000
      }

      binary = CredentialTestResult.encode(result)
      assert {:error, {:invalid_test_id, _}} = Validator.validate_credential_test_result(binary)
    end
  end

  describe "validate_mikrotik_result/1" do
    test "validates valid MikroTik result" do
      sentence = %MikrotikSentence{
        attributes: %{"name" => "ether1", "running" => "true"}
      }

      result = %MikrotikResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        sentences: [sentence],
        error: "",
        timestamp: 1_700_000_000
      }

      binary = MikrotikResult.encode(result)
      assert {:ok, validated} = Validator.validate_mikrotik_result(binary)
      assert length(validated.sentences) == 1
    end

    test "rejects too many sentences" do
      sentences =
        Enum.map(1..1100, fn _ ->
          %MikrotikSentence{attributes: %{}}
        end)

      result = %MikrotikResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        sentences: sentences,
        error: "",
        timestamp: 1_700_000_000
      }

      binary = MikrotikResult.encode(result)
      assert {:error, {:too_many_sentences, _}} = Validator.validate_mikrotik_result(binary)
    end
  end

  describe "validate_heartbeat/1 - hostname length" do
    test "rejects hostname exceeding 255 characters" do
      long_hostname = String.duplicate("a", 256)

      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: long_hostname,
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_hostname, "Hostname too long"}} = Validator.validate_heartbeat(binary)
    end

    test "accepts hostname at exactly 255 characters" do
      # Build a valid RFC 1123 hostname at max length: segments like "a" joined by dots
      # Each segment must start/end with alnum, max 63 chars
      segment = String.duplicate("a", 63)
      # 63 * 4 + 3 dots = 255
      hostname = Enum.join([segment, segment, segment, segment], ".")

      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: hostname,
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end
  end

  describe "validate_heartbeat/1 - version edge cases" do
    test "accepts version with pre-release suffix" do
      heartbeat = %AgentHeartbeat{
        version: "1.2.3-beta.1",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "accepts git-describe version with commit count and SHA" do
      heartbeat = %AgentHeartbeat{
        version: "0.1.0-5-gabcdef0",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, validated} = Validator.validate_heartbeat(binary)
      assert validated.version == "0.1.0-5-gabcdef0"
    end

    test "accepts bare git short SHA as version" do
      heartbeat = %AgentHeartbeat{
        version: "abcdef0",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "accepts 'dev' as version" do
      heartbeat = %AgentHeartbeat{
        version: "dev",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "accepts RFC 3339 timestamp version format" do
      heartbeat = %AgentHeartbeat{
        version: "2025-02-09T15:30:45Z",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, validated} = Validator.validate_heartbeat(binary)
      assert validated.version == "2025-02-09T15:30:45Z"
    end

    test "accepts RFC 3339 timestamp with different date/time" do
      heartbeat = %AgentHeartbeat{
        version: "2026-12-31T23:59:59Z",
        hostname: "agent02",
        uptime_seconds: 200,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "rejects RFC 3339 timestamp without Z suffix" do
      heartbeat = %AgentHeartbeat{
        version: "2025-02-09T15:30:45",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects RFC 3339 timestamp with offset instead of Z" do
      heartbeat = %AgentHeartbeat{
        version: "2025-02-09T15:30:45+00:00",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects version with spaces" do
      heartbeat = %AgentHeartbeat{
        version: "1.2 .3",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects version exceeding 255 characters" do
      long_version = "1.2.3-" <> String.duplicate("a", 250)

      heartbeat = %AgentHeartbeat{
        version: long_version,
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_version, _}} = Validator.validate_heartbeat(binary)
    end
  end

  describe "validate_heartbeat/1 - uptime boundary" do
    test "accepts uptime at max value (4_294_967_295)" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "agent01",
        uptime_seconds: 4_294_967_295,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "rejects uptime at max value + 1" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "agent01",
        uptime_seconds: 4_294_967_296,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_uptime, _}} = Validator.validate_heartbeat(binary)
    end
  end

  describe "validate_metric_batch/1 - empty status" do
    test "accepts empty status on sensor reading (backward compat)" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 23.5,
        status: "",
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_metric_batch/1 - all valid statuses" do
    for status <- ["success", "failure", "up", "down", "ok", "error", "warning"] do
      test "accepts status '#{status}'" do
        reading = %SensorReading{
          sensor_id: "550e8400-e29b-41d4-a716-446655440000",
          value: 23.5,
          status: unquote(status),
          timestamp: 1_700_000_000
        }

        batch = %MetricBatch{
          metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
        }

        binary = MetricBatch.encode(batch)
        assert {:ok, _} = Validator.validate_metric_batch(binary)
      end
    end
  end

  describe "validate_metric_batch/1 - neighbor discovery string lengths" do
    test "rejects remote_chassis_id exceeding 255 characters" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "lldp",
        remote_chassis_id: String.duplicate("x", 256),
        remote_system_name: "",
        remote_system_description: "",
        remote_platform: "",
        remote_port_id: "",
        remote_port_description: "",
        remote_address: "",
        remote_capabilities: [],
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:string_too_long, msg}} = Validator.validate_metric_batch(binary)
      assert msg =~ "remote_chassis_id"
    end

    test "rejects remote_system_description exceeding 10_000 characters" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "lldp",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "switch01",
        remote_system_description: String.duplicate("d", 10_001),
        remote_platform: "",
        remote_port_id: "",
        remote_port_description: "",
        remote_address: "",
        remote_capabilities: [],
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:string_too_long, msg}} = Validator.validate_metric_batch(binary)
      assert msg =~ "remote_system_description"
    end

    test "accepts cdp protocol" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "cdp",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "switch01",
        remote_system_description: "",
        remote_platform: "",
        remote_port_id: "",
        remote_port_description: "",
        remote_address: "",
        remote_capabilities: [],
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_metric_batch/1 - monitoring check edge cases" do
    test "rejects negative response time" do
      check = %MonitoringCheck{
        device_id: "550e8400-e29b-41d4-a716-446655440003",
        status: "success",
        response_time_ms: -1.0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:monitoring_check, check}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_response_time, _}} = Validator.validate_metric_batch(binary)
    end

    test "accepts response time at max boundary (60_000ms)" do
      check = %MonitoringCheck{
        device_id: "550e8400-e29b-41d4-a716-446655440003",
        status: "success",
        response_time_ms: 60_000.0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:monitoring_check, check}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "rejects response time just over max boundary" do
      check = %MonitoringCheck{
        device_id: "550e8400-e29b-41d4-a716-446655440003",
        status: "success",
        response_time_ms: 60_001.0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:monitoring_check, check}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_response_time, _}} = Validator.validate_metric_batch(binary)
    end

    test "accepts zero response time" do
      check = %MonitoringCheck{
        device_id: "550e8400-e29b-41d4-a716-446655440003",
        status: "up",
        response_time_ms: 0.0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:monitoring_check, check}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_metric_batch/1 - timestamp boundaries" do
    test "accepts timestamp at zero" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 1.0,
        status: "ok",
        timestamp: 0
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "accepts timestamp at max value (2_147_483_647)" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 1.0,
        status: "ok",
        timestamp: 2_147_483_647
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "rejects timestamp at max value + 1" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 1.0,
        status: "ok",
        timestamp: 2_147_483_648
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_timestamp, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects negative timestamp" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 1.0,
        status: "ok",
        timestamp: -1
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:sensor_reading, reading}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_timestamp, _}} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_metric_batch/1 - empty batch" do
    test "accepts empty metrics list" do
      batch = %MetricBatch{metrics: []}

      binary = MetricBatch.encode(batch)
      assert {:ok, validated} = Validator.validate_metric_batch(binary)
      assert validated.metrics == []
    end
  end

  describe "validate_metric_batch/1 - multiple metrics" do
    test "validates batch with multiple valid metrics of different types" do
      reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 23.5,
        status: "ok",
        timestamp: 1_700_000_000
      }

      stat = %InterfaceStat{
        interface_id: "550e8400-e29b-41d4-a716-446655440001",
        if_in_octets: 1000,
        if_out_octets: 2000,
        if_in_errors: 0,
        if_out_errors: 0,
        if_in_discards: 0,
        if_out_discards: 0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:sensor_reading, reading}},
          %Metric{metric_type: {:interface_stat, stat}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, validated} = Validator.validate_metric_batch(binary)
      assert length(validated.metrics) == 2
    end

    test "rejects batch when second metric is invalid" do
      valid_reading = %SensorReading{
        sensor_id: "550e8400-e29b-41d4-a716-446655440000",
        value: 23.5,
        status: "ok",
        timestamp: 1_700_000_000
      }

      invalid_reading = %SensorReading{
        sensor_id: "not-a-uuid",
        value: 1.0,
        status: "ok",
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [
          %Metric{metric_type: {:sensor_reading, valid_reading}},
          %Metric{metric_type: {:sensor_reading, invalid_reading}}
        ]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_sensor_id, _}} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_snmp_result/1 - timestamp" do
    test "rejects SNMP result with negative timestamp" do
      result = %SnmpResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_type: 1,
        oid_values: %{},
        timestamp: -1
      }

      binary = SnmpResult.encode(result)
      assert {:error, {:invalid_timestamp, _}} = Validator.validate_snmp_result(binary)
    end

    test "rejects SNMP result with timestamp exceeding max" do
      result = %SnmpResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_type: 1,
        oid_values: %{},
        timestamp: 2_147_483_648
      }

      binary = SnmpResult.encode(result)
      assert {:error, {:invalid_timestamp, _}} = Validator.validate_snmp_result(binary)
    end
  end

  describe "validate_agent_error/1 - edge cases" do
    test "rejects empty job_id" do
      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "",
        message: "Some error"
      }

      binary = AgentError.encode(error)
      assert {:error, {:invalid_job_id, _}} = Validator.validate_agent_error(binary)
    end

    test "accepts job_id at 255 characters" do
      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: String.duplicate("j", 255),
        message: "Some error"
      }

      binary = AgentError.encode(error)
      assert {:ok, _} = Validator.validate_agent_error(binary)
    end

    test "rejects job_id exceeding 255 characters" do
      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: String.duplicate("j", 256),
        message: "Some error"
      }

      binary = AgentError.encode(error)
      assert {:error, {:invalid_job_id, _}} = Validator.validate_agent_error(binary)
    end

    test "accepts empty error message" do
      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        message: ""
      }

      binary = AgentError.encode(error)
      assert {:ok, _} = Validator.validate_agent_error(binary)
    end

    test "accepts error message at exactly 1000 characters" do
      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        message: String.duplicate("e", 1000)
      }

      binary = AgentError.encode(error)
      assert {:ok, _} = Validator.validate_agent_error(binary)
    end

    test "rejects error message at 1001 characters" do
      error = %AgentError{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        message: String.duplicate("e", 1001)
      }

      binary = AgentError.encode(error)
      assert {:error, {:string_too_long, _}} = Validator.validate_agent_error(binary)
    end
  end

  describe "validate_credential_test_result/1 - system description length" do
    test "rejects system description exceeding 10_000 characters" do
      result = %CredentialTestResult{
        test_id: "550e8400-e29b-41d4-a716-446655440000",
        success: true,
        error_message: "",
        system_description: String.duplicate("d", 10_001),
        timestamp: 1_700_000_000
      }

      binary = CredentialTestResult.encode(result)
      assert {:error, {:string_too_long, msg}} = Validator.validate_credential_test_result(binary)
      assert msg =~ "System description"
    end

    test "accepts system description at exactly 10_000 characters" do
      result = %CredentialTestResult{
        test_id: "550e8400-e29b-41d4-a716-446655440000",
        success: true,
        error_message: "",
        system_description: String.duplicate("d", 10_000),
        timestamp: 1_700_000_000
      }

      binary = CredentialTestResult.encode(result)
      assert {:ok, _} = Validator.validate_credential_test_result(binary)
    end
  end

  describe "validate_credential_test_result/1 - error message length" do
    test "rejects error message exceeding 1000 characters" do
      result = %CredentialTestResult{
        test_id: "550e8400-e29b-41d4-a716-446655440000",
        success: false,
        error_message: String.duplicate("e", 1001),
        system_description: "",
        timestamp: 1_700_000_000
      }

      binary = CredentialTestResult.encode(result)
      assert {:error, {:string_too_long, _}} = Validator.validate_credential_test_result(binary)
    end
  end

  describe "validate_credential_test_result/1 - timestamp" do
    test "rejects invalid timestamp" do
      result = %CredentialTestResult{
        test_id: "550e8400-e29b-41d4-a716-446655440000",
        success: true,
        error_message: "",
        system_description: "",
        timestamp: 9_999_999_999
      }

      binary = CredentialTestResult.encode(result)
      assert {:error, {:invalid_timestamp, _}} = Validator.validate_credential_test_result(binary)
    end
  end

  describe "validate_mikrotik_result/1 - edge cases" do
    test "rejects invalid device_id" do
      result = %MikrotikResult{
        device_id: "not-valid",
        job_id: "job123",
        sentences: [],
        error: "",
        timestamp: 1_700_000_000
      }

      binary = MikrotikResult.encode(result)
      assert {:error, {:invalid_device_id, _}} = Validator.validate_mikrotik_result(binary)
    end

    test "rejects empty job_id" do
      result = %MikrotikResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "",
        sentences: [],
        error: "",
        timestamp: 1_700_000_000
      }

      binary = MikrotikResult.encode(result)
      assert {:error, {:invalid_job_id, _}} = Validator.validate_mikrotik_result(binary)
    end

    test "rejects error message exceeding 1000 characters" do
      result = %MikrotikResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        sentences: [],
        error: String.duplicate("e", 1001),
        timestamp: 1_700_000_000
      }

      binary = MikrotikResult.encode(result)
      assert {:error, {:string_too_long, _}} = Validator.validate_mikrotik_result(binary)
    end

    test "rejects invalid timestamp" do
      result = %MikrotikResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        sentences: [],
        error: "",
        timestamp: 9_999_999_999
      }

      binary = MikrotikResult.encode(result)
      assert {:error, {:invalid_timestamp, _}} = Validator.validate_mikrotik_result(binary)
    end

    test "accepts empty sentences list" do
      result = %MikrotikResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        sentences: [],
        error: "",
        timestamp: 1_700_000_000
      }

      binary = MikrotikResult.encode(result)
      assert {:ok, _} = Validator.validate_mikrotik_result(binary)
    end

    test "accepts sentences at exactly 1000" do
      sentences = Enum.map(1..1000, fn _ -> %MikrotikSentence{attributes: %{}} end)

      result = %MikrotikResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_id: "job123",
        sentences: sentences,
        error: "",
        timestamp: 1_700_000_000
      }

      binary = MikrotikResult.encode(result)
      assert {:ok, _} = Validator.validate_mikrotik_result(binary)
    end
  end

  describe "validate_metric_batch/1 - interface stat counter boundaries" do
    test "rejects negative if_out_errors" do
      stat = %InterfaceStat{
        interface_id: "550e8400-e29b-41d4-a716-446655440001",
        if_in_octets: 0,
        if_out_octets: 0,
        if_in_errors: 0,
        if_out_errors: -1,
        if_in_discards: 0,
        if_out_discards: 0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:interface_stat, stat}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects negative if_in_discards" do
      stat = %InterfaceStat{
        interface_id: "550e8400-e29b-41d4-a716-446655440001",
        if_in_octets: 0,
        if_out_octets: 0,
        if_in_errors: 0,
        if_out_errors: 0,
        if_in_discards: -5,
        if_out_discards: 0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:interface_stat, stat}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects negative if_out_discards" do
      stat = %InterfaceStat{
        interface_id: "550e8400-e29b-41d4-a716-446655440001",
        if_in_octets: 0,
        if_out_octets: 0,
        if_in_errors: 0,
        if_out_errors: 0,
        if_in_discards: 0,
        if_out_discards: -1,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:interface_stat, stat}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_counter, _}} = Validator.validate_metric_batch(binary)
    end

    test "rejects invalid interface UUID" do
      stat = %InterfaceStat{
        interface_id: "bad-uuid",
        if_in_octets: 0,
        if_out_octets: 0,
        if_in_errors: 0,
        if_out_errors: 0,
        if_in_discards: 0,
        if_out_discards: 0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:interface_stat, stat}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_interface_id, _}} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_metric_batch/1 - neighbor discovery with remote IPv6" do
    test "accepts remote_address as IPv6" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "lldp",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "switch01",
        remote_system_description: "",
        remote_platform: "",
        remote_port_id: "",
        remote_port_description: "",
        remote_address: "fe80::1",
        remote_capabilities: [],
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "rejects invalid remote_address IP" do
      discovery = %NeighborDiscovery{
        interface_id: "550e8400-e29b-41d4-a716-446655440002",
        protocol: "lldp",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "switch01",
        remote_system_description: "",
        remote_platform: "",
        remote_port_id: "",
        remote_port_description: "",
        remote_address: "not-an-ip",
        remote_capabilities: [],
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:neighbor_discovery, discovery}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_ip, _}} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_metric_batch/1 - monitoring check with invalid device_id" do
    test "rejects monitoring check with non-UUID device_id" do
      check = %MonitoringCheck{
        device_id: "not-valid",
        status: "success",
        response_time_ms: 1.0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:monitoring_check, check}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:error, {:invalid_device_id, _}} = Validator.validate_metric_batch(binary)
    end
  end

  describe "validate_snmp_result/1 - malformed protobuf" do
    test "rejects malformed protobuf binary" do
      assert {:error, {:decode_error, _}} = Validator.validate_snmp_result(<<255, 255, 255>>)
    end
  end

  describe "validate_agent_error/1 - malformed protobuf" do
    test "rejects malformed protobuf binary" do
      assert {:error, {:decode_error, _}} = Validator.validate_agent_error(<<255, 255, 255>>)
    end
  end

  describe "validate_credential_test_result/1 - malformed protobuf" do
    test "rejects malformed protobuf binary" do
      assert {:error, {:decode_error, _}} = Validator.validate_credential_test_result(<<255, 255, 255>>)
    end
  end

  describe "validate_mikrotik_result/1 - malformed protobuf" do
    test "rejects malformed protobuf binary" do
      assert {:error, {:decode_error, _}} = Validator.validate_mikrotik_result(<<255, 255, 255>>)
    end
  end

  describe "validate_metric_batch/1 - malformed protobuf" do
    test "rejects malformed protobuf binary" do
      assert {:error, {:decode_error, _}} = Validator.validate_metric_batch(<<255, 255, 255>>)
    end
  end

  describe "edge cases" do
    test "handles empty strings gracefully" do
      heartbeat = %AgentHeartbeat{
        version: "",
        hostname: "",
        uptime_seconds: 0,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      # Empty version should be accepted (backward compat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "handles zero values" do
      stat = %InterfaceStat{
        interface_id: "550e8400-e29b-41d4-a716-446655440001",
        if_in_octets: 0,
        if_out_octets: 0,
        if_in_errors: 0,
        if_out_errors: 0,
        if_in_discards: 0,
        if_out_discards: 0,
        timestamp: 1_700_000_000
      }

      batch = %MetricBatch{
        metrics: [%Metric{metric_type: {:interface_stat, stat}}]
      }

      binary = MetricBatch.encode(batch)
      assert {:ok, _} = Validator.validate_metric_batch(binary)
    end

    test "handles empty collections" do
      result = %SnmpResult{
        device_id: "550e8400-e29b-41d4-a716-446655440000",
        job_type: 1,
        oid_values: %{},
        timestamp: 1_700_000_000
      }

      binary = SnmpResult.encode(result)
      assert {:ok, _} = Validator.validate_snmp_result(binary)
    end

    test "accepts UUID with uppercase hex characters" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "agent01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)

      # Test with uppercase UUID in device_id
      result = %SnmpResult{
        device_id: "550E8400-E29B-41D4-A716-446655440000",
        job_type: 1,
        oid_values: %{},
        timestamp: 1_700_000_000
      }

      binary = SnmpResult.encode(result)
      assert {:ok, _} = Validator.validate_snmp_result(binary)
    end

    test "handles hostname with hyphens" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "my-agent-01",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:ok, _} = Validator.validate_heartbeat(binary)
    end

    test "rejects hostname starting with hyphen" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "-invalid",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects hostname ending with hyphen" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "invalid-",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
    end

    test "rejects hostname with special characters" do
      heartbeat = %AgentHeartbeat{
        version: "1.0.0",
        hostname: "host_name@bad",
        uptime_seconds: 100,
        ip_address: ""
      }

      binary = AgentHeartbeat.encode(heartbeat)
      assert {:error, {:invalid_hostname, _}} = Validator.validate_heartbeat(binary)
    end
  end
end
