defmodule Towerops.Profiles.YamlProfilesTest do
  use Towerops.DataCase, async: false

  import Mox

  alias Towerops.Profiles.MibCache
  alias Towerops.Profiles.YamlProfiles
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

  describe "match_profile/2 with snmpget conditions" do
    test "matches airos profile when af60Role.1 OID is missing (= false semantics)" do
      # Ubiquiti device with OID 1.3.6.1.4.1.10002.1 and sysDescr containing "Linux"
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.10002.1",
        sys_descr: "Linux 2.6.32.68 #1 Fri Dec 12 09:47:27 Europe 2025 mips"
      }

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      # Mock the SNMP query for af60Role.1 to fail (OID doesn't exist)
      # This simulates the fallback behavior where missing OID should be treated as false
      # Use stub to allow multiple calls since multiple profiles will try to match
      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      # Also stub walk for profiles that use snmpwalk conditions
      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      # Should match the 'airos' profile because:
      # 1. OID matches 1.3.6.1.4.1.10002.1
      # 2. sysDescr contains "Linux"
      # 3. snmpget condition: af60Role.1 = false
      # 4. OID is missing, so treated as false → condition matches
      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "airos"
    end

    test "does not match airos-af profile when fwVersion.1 OID is missing (!= false semantics)" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.10002.1",
        sys_descr: "Linux 2.6.32.68"
      }

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      # Mock SNMP queries - all OIDs missing
      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      # The 'airos-af' profile requires fwVersion.1 != false
      # When OID is missing, the condition should NOT match
      # Device should fall through to 'airos' fallback
      profile = YamlProfiles.match_profile(system_info, client_opts)

      # Should match airos, not airos-af
      assert profile
      assert profile.name == "airos"
      refute profile.name == "airos-af"
    end

    test "matches airos profile when af60Role.1 exists and equals false" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.10002.1",
        sys_descr: "Linux 3.10.0"
      }

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      # Mock: af60Role.1 exists and returns false
      stub(SnmpMock, :get, fn _target, oid, _opts ->
        case oid do
          "UI-AF60-MIB::af60Role.1" -> {:ok, false}
          "af60Role.1" -> {:ok, false}
          "1.3.6.1.4.1.41112.1.1.1.1.1.0" -> {:ok, false}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "airos"
    end

    test "does not match airos profile when af60Role.1 exists and equals true" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.10002.1",
        sys_descr: "Linux 3.10.0"
      }

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      # Mock: af60Role.1 exists and returns true (not false)
      stub(SnmpMock, :get, fn _target, oid, _opts ->
        case oid do
          # Match both MIB name and numeric OID (in case resolution works/doesn't work)
          "UI-AF60-MIB::af60Role.1" -> {:ok, true}
          "af60Role.1" -> {:ok, true}
          "1.3.6.1.4.1.41112.1.1.1.1.1.0" -> {:ok, true}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      # Should not match airos because condition is af60Role.1 = false
      # With value = true, condition fails
      # Should return nil (no profile matches)
      profile = YamlProfiles.match_profile(system_info, client_opts)

      # Since all other profiles also fail, should return nil
      assert profile == nil
    end

    test "matches airos-af profile when fwVersion.1 OID exists (!= false semantics)" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.10002.1",
        sys_descr: "Linux 3.10.0"
      }

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      # Mock: fwVersion.1 exists (AirFiber device)
      stub(SnmpMock, :get, fn _target, oid, _opts ->
        case oid do
          # UBNT-AirFIBER-MIB::fwVersion.1 (match MIB name and numeric OID)
          "UBNT-AirFIBER-MIB::fwVersion.1" -> {:ok, "v3.2.1"}
          "fwVersion.1" -> {:ok, "v3.2.1"}
          "1.3.6.1.4.1.41112.1.3.1.1.0" -> {:ok, "v3.2.1"}
          # af60Role.1 doesn't exist
          "UI-AF60-MIB::af60Role.1" -> {:error, :no_such_object}
          "af60Role.1" -> {:error, :no_such_object}
          "1.3.6.1.4.1.41112.1.1.1.1.1.0" -> {:error, :no_such_object}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "airos-af"
    end
  end

  describe "match_profile/2 without snmpget conditions" do
    test "matches infinity profile with sysDescr regex (unconditional)" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.10002.1",
        sys_descr: "NFT 2 Wireless System"
      }

      # No SNMP queries should be made for unconditional match
      profile = YamlProfiles.match_profile(system_info, [])

      assert profile
      assert profile.name == "infinity"
    end

    test "matches unifi profile by sysDescr regex (first detection block)" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.41112",
        sys_descr: "UAP-AC-Pro"
      }

      profile = YamlProfiles.match_profile(system_info, [])

      assert profile
      assert profile.name == "unifi"
    end

    test "returns nil when no profile matches" do
      system_info = %{
        sys_object_id: "1.2.3.4.5.6.7.8.9",
        sys_descr: "Unknown Device"
      }

      # Provide client_opts in case conditional profiles try to match
      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      # Mock SNMP to return errors for all OIDs
      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile == nil
    end
  end

  describe "error handling" do
    test "application survives malformed YAML files in profiles directory" do
      # This test ensures that malformed YAML files don't crash the GenServer during startup.
      # The bug: load_profile/2 returns {:error, ...} tuples which end up in the profiles list,
      # then extract_mib_names_from_profiles/1 tries to do profile["discovery"] on error tuples,
      # causing FunctionClauseError in Access.get/3.

      # Create a temporary malformed YAML file in the profiles directory
      profiles_path = Path.join(:code.priv_dir(:towerops), "profiles/os_detection")
      malformed_file = Path.join(profiles_path, "test_malformed.yaml")

      # Write truly malformed YAML (unclosed bracket)
      File.write!(malformed_file, "sysObjectID: [\n  - 1.2.3.4.5\ntext: bad")

      try do
        # Verify that YamlProfiles GenServer is still running and functional
        # despite the malformed YAML file existing
        assert Process.whereis(YamlProfiles)

        # Verify we can still match profiles (the GenServer didn't crash)
        system_info = %{
          sys_object_id: "1.3.6.1.4.1.10002.1",
          sys_descr: "Linux"
        }

        client_opts = [
          ip: "192.168.1.1",
          community: "public",
          version: "2c",
          port: 161
        ]

        stub(SnmpMock, :get, fn _target, _oid, _opts ->
          {:error, :no_such_object}
        end)

        stub(SnmpMock, :walk, fn _target, _oid, _opts ->
          {:error, :no_such_object}
        end)

        # Should be able to match profiles without crashing
        _profile = YamlProfiles.match_profile(system_info, client_opts)

        # The test passes if we get here without crashing
      after
        # Cleanup: remove the malformed file
        File.rm(malformed_file)
      end
    end
  end

  describe "MIB name caching" do
    test "creates :mib_cache ETS table on startup" do
      # Verify table exists (created by MibCache GenServer)
      assert :ets.whereis(:mib_cache) != :undefined
    end

    test "pre-resolves MIB names from profiles at startup" do
      # MibCache pre-resolution happens asynchronously in the background
      # In test environment, ToweropsNative.resolve_oid may timeout, so we
      # just verify the cache table exists and lookups work (with runtime resolution)
      assert :ets.whereis(:mib_cache) != :undefined

      # Verify lookups work (will do runtime resolution if not pre-resolved)
      result = MibCache.lookup("sysDescr")
      assert is_binary(result)
    end

    test "uses cached OIDs during conditional matching" do
      # Don't wait for pre-resolution - MibCache.lookup handles cache misses
      # by doing runtime resolution with timeout protection

      system_info = %{
        sys_object_id: "1.3.6.1.4.1.10002.1",
        sys_descr: "Linux 2.6.32.68"
      }

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      # Mock: Expect numeric OID (not MIB name with ::)
      # The cache returns OIDs as-is in test environment to avoid hanging
      stub(SnmpMock, :get, fn _target, oid, _opts ->
        # Verify we're not getting symbolic MIB names (those contain "::")
        if String.contains?(oid, "::") do
          flunk("Expected numeric OID but got symbolic MIB name: #{oid}")
        end

        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      _profile = YamlProfiles.match_profile(system_info, client_opts)

      # Test passed if we didn't flunk above (no symbolic names with ::)
    end

    test "handles cache miss gracefully" do
      # MibCache.lookup/1 handles cache misses by resolving at runtime
      # This test verifies the lookup returns a value for unknown MIB names
      result = MibCache.lookup("TEST-MIB::testObject")
      assert is_binary(result)
    end
  end

  describe "HP Comware profile" do
    test "matches comware profile with H3C sysDescr" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.25506.11.1.172",
        sys_descr: "Version 7.1.070, Release 3208P03\nH3C S5130-54S-EI-PWR"
      }

      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "comware"
    end

    test "matches comware profile with HPE Comware sysDescr" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.25506.11.1.172",
        sys_descr: "Version 7.1.045, Release 2432P01\nHPE 5130-48G-PoE+-4SFP+ EI Switch"
      }

      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "comware"
    end

    test "matches comware profile with HP Comware sysDescr" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.25506.11.1.172",
        sys_descr: "Version 5.20.99, Release 1208\nHP A5120-24G-PoE+ Switch"
      }

      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "comware"
    end

    test "comware profile includes sensor definitions" do
      # Load the comware profile directly
      profiles_path = Path.join(:code.priv_dir(:towerops), "profiles/os_discovery")
      yaml_path = Path.join(profiles_path, "comware.yaml")

      assert File.exists?(yaml_path), "comware.yaml profile should exist"

      {:ok, content} = File.read(yaml_path)
      {:ok, profile} = YamlElixir.read_from_string(content)

      # Verify the profile has sensor definitions
      assert get_in(profile, ["modules", "sensors"])
      assert get_in(profile, ["modules", "sensors", "state"])
      assert get_in(profile, ["modules", "sensors", "power"])
      assert get_in(profile, ["modules", "sensors", "temperature"])

      # Verify fan status sensor
      fan_sensors = get_in(profile, ["modules", "sensors", "state", "data"])
      assert Enum.any?(fan_sensors, fn sensor -> sensor["descr"] =~ ~r/Fan/ end)

      # Verify power supply sensor
      power_sensors = get_in(profile, ["modules", "sensors", "state", "data"])
      assert Enum.any?(power_sensors, fn sensor -> sensor["descr"] =~ ~r/Power Supply/ end)
    end
  end

  describe "HP ProCurve profile" do
    test "matches procurve profile with HP sysDescr" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.11.2.3.7.11.119",
        sys_descr:
          "HP J9728A 2920-48G-POE+ Switch, revision WB.16.10.0012, ROM WB.16.03 (/ws/swbuildm/rel_boston_qaoff/code/build/anm(swbuildm_rel_boston_qaoff_rel_boston))"
      }

      # ProCurve profiles may have snmpget conditions, so stub SNMP
      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "procurve"
    end

    test "matches procurve profile with ProCurve branded device" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.11.2.3.7.11.33",
        sys_descr: "ProCurve J9019B Switch 2510-24, revision T.11.30, ROM T.11.01 (/sw/code/build/fish(t3a))"
      }

      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "procurve"
    end

    test "matches procurve profile with Aruba branded switch" do
      system_info = %{
        sys_object_id: "1.3.6.1.4.1.11.2.3.7.11.157",
        sys_descr:
          "Aruba 2930M-48G-PoE+, revision WC.16.11.0010, ROM WC.16.03 (/ws/swbuildm/rel_maui_qaoff/code/build/anm(swbuildm_rel_maui_qaoff_rel_maui))"
      }

      stub(SnmpMock, :get, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      stub(SnmpMock, :walk, fn _target, _oid, _opts ->
        {:error, :no_such_object}
      end)

      client_opts = [
        ip: "192.168.1.1",
        community: "public",
        version: "2c",
        port: 161
      ]

      profile = YamlProfiles.match_profile(system_info, client_opts)

      assert profile
      assert profile.name == "procurve"
    end

    test "procurve profile includes mempool definitions" do
      profiles_path = Path.join(:code.priv_dir(:towerops), "profiles/os_discovery")
      yaml_path = Path.join(profiles_path, "procurve.yaml")

      assert File.exists?(yaml_path), "procurve.yaml profile should exist"

      {:ok, content} = File.read(yaml_path)
      {:ok, profile} = YamlElixir.read_from_string(content)

      # Verify mempools
      assert get_in(profile, ["modules", "mempools"])
      mempool_data = get_in(profile, ["modules", "mempools", "data"])
      assert length(mempool_data) >= 2

      # Verify local and global memory types
      assert Enum.any?(mempool_data, fn mp -> mp["type"] == "hpLocal" end)
      assert Enum.any?(mempool_data, fn mp -> mp["type"] == "hpGlobal" end)
    end

    test "procurve profile includes processor definitions" do
      profiles_path = Path.join(:code.priv_dir(:towerops), "profiles/os_discovery")
      yaml_path = Path.join(profiles_path, "procurve.yaml")

      {:ok, content} = File.read(yaml_path)
      {:ok, profile} = YamlElixir.read_from_string(content)

      # Verify processors
      assert get_in(profile, ["modules", "processors"])
      processor_data = get_in(profile, ["modules", "processors", "data"])
      assert processor_data != []

      # Verify CPU type
      assert Enum.any?(processor_data, fn proc -> proc["type"] == "procurve-fixed" end)
    end

    test "procurve profile includes comprehensive sensor definitions" do
      profiles_path = Path.join(:code.priv_dir(:towerops), "profiles/os_discovery")
      yaml_path = Path.join(profiles_path, "procurve.yaml")

      {:ok, content} = File.read(yaml_path)
      {:ok, profile} = YamlElixir.read_from_string(content)

      # Verify sensor types exist
      assert get_in(profile, ["modules", "sensors", "temperature"])
      assert get_in(profile, ["modules", "sensors", "power"])
      assert get_in(profile, ["modules", "sensors", "state"])
      assert get_in(profile, ["modules", "sensors", "current"])
      assert get_in(profile, ["modules", "sensors", "voltage"])
      assert get_in(profile, ["modules", "sensors", "dbm"])

      # Verify fan state sensors
      state_sensors = get_in(profile, ["modules", "sensors", "state", "data"])
      assert Enum.any?(state_sensors, fn sensor -> sensor["state_name"] == "hpicfFanState" end)

      # Verify power supply state sensors
      assert Enum.any?(state_sensors, fn sensor -> sensor["state_name"] == "hpicfPsState" end)

      # Verify stack state sensors
      assert Enum.any?(state_sensors, fn sensor -> sensor["state_name"] == "hpStackOperStatus" end)

      # Verify PoE power sensors
      power_sensors = get_in(profile, ["modules", "sensors", "power", "data"])
      assert Enum.any?(power_sensors, fn sensor -> sensor["descr"] =~ ~r/PoE/ end)

      # Verify transceiver sensors (temperature, current, voltage, dbm)
      temp_sensors = get_in(profile, ["modules", "sensors", "temperature", "data"])
      assert Enum.any?(temp_sensors, fn sensor -> sensor["descr"] =~ ~r/Transceiver/ end)
    end
  end
end
