defmodule Towerops.Monitoring.PingTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  alias Towerops.Monitoring.Ping

  # These tests focus on IP validation which happens BEFORE any system calls
  describe "IP address validation" do
    test "returns error for invalid IP address" do
      assert {:error, :invalid_ip} = Ping.ping("not-an-ip")
    end

    test "returns error for empty string" do
      assert {:error, :invalid_ip} = Ping.ping("")
    end

    test "returns error for malformed IP with out of range octets" do
      assert {:error, :invalid_ip} = Ping.ping("256.256.256.256")
    end

    test "returns error for partial IP format" do
      # Note: "192.168.1" is valid shorthand (192.168.0.1), so use trailing dot
      assert {:error, :invalid_ip} = Ping.ping("192.168.1.")
    end

    test "returns error for IP with extra octets" do
      assert {:error, :invalid_ip} = Ping.ping("192.168.1.1.1")
    end

    test "returns error for IP with letters" do
      assert {:error, :invalid_ip} = Ping.ping("192.168.1.abc")
    end

    test "returns error for IP with special characters" do
      assert {:error, :invalid_ip} = Ping.ping("192.168.1.1!")
    end

    test "rejects invalid IPv6 address" do
      assert {:error, :invalid_ip} = Ping.ping("2001:0db8:85a3::8a2e:0370:gggg")
    end

    test "rejects malformed IPv6 address" do
      assert {:error, :invalid_ip} = Ping.ping(":::1")
    end
  end

  describe "error handling" do
    test "handles nil IP address with FunctionClauseError" do
      assert_raise FunctionClauseError, fn ->
        Ping.ping(nil)
      end
    end

    test "handles integer IP address with FunctionClauseError" do
      assert_raise FunctionClauseError, fn ->
        Ping.ping(123)
      end
    end

    test "handles atom IP address with FunctionClauseError" do
      assert_raise FunctionClauseError, fn ->
        Ping.ping(:localhost)
      end
    end

    test "handles list IP address with FunctionClauseError" do
      assert_raise FunctionClauseError, fn ->
        Ping.ping([192, 168, 1, 1])
      end
    end

    test "handles non-integer timeout with FunctionClauseError" do
      assert_raise FunctionClauseError, fn ->
        Ping.ping("127.0.0.1", "1000")
      end
    end
  end

  describe "PingBehaviour implementation" do
    test "implements ping/1 callback" do
      exports = Ping.module_info(:exports)
      assert {:ping, 1} in exports
    end

    test "implements ping/2 callback" do
      exports = Ping.module_info(:exports)
      assert {:ping, 2} in exports
    end
  end

  describe "property-based tests for IP validation" do
    property "invalid IP strings always return error from validation" do
      # Use generators that produce definitely-invalid IPs (no expensive filter)
      check all(
              invalid_ip <-
                one_of([
                  # Alphanumeric strings (no dots) are never valid IPs
                  string(:alphanumeric, min_length: 1, max_length: 15),
                  # IPs with invalid octets (>255)
                  map(integer(256..999), &"#{&1}.#{&1}.#{&1}.#{&1}"),
                  # IPs with letters in octet position (use ?a..?z range)
                  map(string(?a..?z, length: 3), &"192.168.#{&1}.1")
                ]),
              max_runs: 10
            ) do
        # These fail at validation, before any system call
        result = Ping.ping(invalid_ip)
        assert match?({:error, _}, result)
      end
    end

    property "valid IPv4 addresses are accepted by inet parser" do
      check all(
              octet1 <- integer(0..255),
              octet2 <- integer(0..255),
              octet3 <- integer(0..255),
              octet4 <- integer(0..255),
              max_runs: 20
            ) do
        ip = "#{octet1}.#{octet2}.#{octet3}.#{octet4}"
        # Just verify IP validation passes (don't actually ping)
        assert {:ok, _} = :inet.parse_address(String.to_charlist(ip))
      end
    end
  end

  # These tests verify the ping output parser behavior
  # by testing the module's internal parsing logic indirectly
  describe "ping output parsing (via mocked system calls)" do
    # We can't easily mock System.cmd, but we can test that the module
    # correctly handles various response formats by testing with valid IPs
    # that return quickly (localhost)

    @tag :integration
    test "localhost ping returns valid response format" do
      result = Ping.ping("127.0.0.1", 2000)

      case result do
        {:ok, response_time} ->
          assert is_float(response_time)
          assert response_time >= 0.0
          assert response_time < 2000.0

        {:error, reason} ->
          # Acceptable errors in test environment
          assert reason in [:timeout, :command_not_found, :parse_error]
      end
    end

    @tag :integration
    test "IPv6 localhost ping returns valid response format" do
      result = Ping.ping("::1", 2000)

      case result do
        {:ok, response_time} ->
          assert is_float(response_time)
          assert response_time >= 0.0

        {:error, reason} ->
          # IPv6 may not be available in all test environments
          assert reason in [:timeout, :command_not_found, :parse_error]
      end
    end
  end
end
