defmodule Towerops.Devices.CredentialResolverTest do
  use ExUnit.Case, async: true

  import Ecto.Changeset

  alias Towerops.Devices.CredentialResolver
  alias Towerops.Devices.Device
  alias Towerops.Organizations.Organization
  alias Towerops.Sites.Site

  # Helper to build an org struct with credential fields
  defp build_org(attrs) do
    defaults = %Organization{
      id: Ecto.UUID.generate(),
      name: "Test Org",
      slug: "testorg",
      snmp_community: "org_public",
      snmp_version: "2c",
      snmp_port: 161,
      snmpv3_username: nil,
      snmpv3_security_level: nil,
      snmpv3_auth_protocol: "SHA-256",
      snmpv3_auth_password: nil,
      snmpv3_priv_protocol: "AES",
      snmpv3_priv_password: nil,
      mikrotik_username: nil,
      mikrotik_password: nil,
      mikrotik_port: 8729,
      mikrotik_ssh_port: 22,
      mikrotik_use_ssl: true,
      mikrotik_enabled: false
    }

    struct(defaults, attrs)
  end

  # Helper to build a site struct with credential fields
  defp build_site(attrs) do
    defaults = %Site{
      id: Ecto.UUID.generate(),
      name: "Test Site",
      organization_id: Ecto.UUID.generate(),
      snmp_community: nil,
      snmp_version: nil,
      snmp_port: nil,
      snmpv3_username: nil,
      snmpv3_security_level: nil,
      snmpv3_auth_protocol: nil,
      snmpv3_auth_password: nil,
      snmpv3_priv_protocol: nil,
      snmpv3_priv_password: nil,
      mikrotik_username: nil,
      mikrotik_password: nil,
      mikrotik_port: nil,
      mikrotik_ssh_port: nil,
      mikrotik_use_ssl: nil,
      mikrotik_enabled: nil
    }

    struct(defaults, attrs)
  end

  # Helper to build a device changeset with given attrs.
  # Uses cast so only explicitly provided attrs become changes.
  # Note: Device schema has defaults for snmp_version ("2c"), snmp_port (161),
  # snmpv3_credential_source ("site"), mikrotik_credential_source ("site").
  # get_field returns schema defaults when no change is present.
  defp device_changeset(attrs \\ %{}) do
    cast(%Device{}, attrs, [
      :snmp_community,
      :snmp_version,
      :snmp_port,
      :snmp_community_source,
      :snmpv3_username,
      :snmpv3_security_level,
      :snmpv3_auth_protocol,
      :snmpv3_auth_password,
      :snmpv3_priv_protocol,
      :snmpv3_priv_password,
      :snmpv3_credential_source,
      :mikrotik_username,
      :mikrotik_password,
      :mikrotik_port,
      :mikrotik_ssh_port,
      :mikrotik_use_ssl,
      :mikrotik_enabled,
      :mikrotik_credential_source
    ])
  end

  # Helper to build a device changeset where schema defaults are nil,
  # allowing the resolver to resolve from hierarchy.
  # This simulates a "blank slate" device with no preexisting defaults.
  defp bare_device_changeset(attrs \\ %{}) do
    cast({%{}, device_types()}, attrs, Map.keys(device_types()))
  end

  defp device_types do
    %{
      snmp_community: :string,
      snmp_version: :string,
      snmp_port: :integer,
      snmp_community_source: :string,
      snmpv3_username: :string,
      snmpv3_security_level: :string,
      snmpv3_auth_protocol: :string,
      snmpv3_auth_password: :string,
      snmpv3_priv_protocol: :string,
      snmpv3_priv_password: :string,
      snmpv3_credential_source: :string,
      mikrotik_username: :string,
      mikrotik_password: :string,
      mikrotik_port: :integer,
      mikrotik_ssh_port: :integer,
      mikrotik_use_ssl: :boolean,
      mikrotik_enabled: :boolean,
      mikrotik_credential_source: :string
    }
  end

  # ── resolve_snmp_credentials/3 - community resolution ──

  describe "resolve_snmp_credentials/3 community resolution" do
    test "inherits community from organization when device has nil" do
      org = build_org(snmp_community: "org_secret")
      changeset = device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_community) == "org_secret"
      assert get_field(result, :snmp_community_source) == "organization"
    end

    test "inherits community from organization when device has empty string" do
      org = build_org(snmp_community: "org_secret")
      changeset = device_changeset(%{snmp_community: ""})

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_community) == "org_secret"
      assert get_field(result, :snmp_community_source) == "organization"
    end

    test "keeps device community when explicitly set" do
      org = build_org(snmp_community: "org_secret")
      changeset = device_changeset(%{snmp_community: "device_secret"})

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_community) == "device_secret"
      assert get_field(result, :snmp_community_source) == "device"
    end

    test "prefers site community over organization" do
      org = build_org(snmp_community: "org_secret")
      site = build_site(snmp_community: "site_secret")
      changeset = device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, site)

      assert get_field(result, :snmp_community) == "site_secret"
      assert get_field(result, :snmp_community_source) == "site"
    end

    test "falls back to organization when site community is nil" do
      org = build_org(snmp_community: "org_secret")
      site = build_site(snmp_community: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, site)

      assert get_field(result, :snmp_community) == "org_secret"
      assert get_field(result, :snmp_community_source) == "organization"
    end

    test "sets source to organization when both site and org community are nil" do
      org = build_org(snmp_community: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_community) == nil
      assert get_field(result, :snmp_community_source) == "organization"
    end

    test "works with nil site" do
      org = build_org(snmp_community: "org_public")
      changeset = device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_community) == "org_public"
      assert get_field(result, :snmp_community_source) == "organization"
    end
  end

  # ── SNMP version resolution (via resolve_snmp_credentials/3) ──
  #
  # Device schema has `snmp_version: "2c"` default. The resolver only resolves
  # from hierarchy when get_field returns nil, so it keeps the schema default
  # ("2c") when no explicit version is cast into the changeset.
  # To test hierarchy resolution, we use bare_device_changeset (no schema defaults).

  describe "resolve_snmp_credentials/3 snmp_version resolution" do
    test "resolves version from organization when field is nil" do
      org = build_org(snmp_version: "1")
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_version) == "1"
    end

    test "defaults to 2c when neither site nor org has version" do
      org = build_org(snmp_version: nil)
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_version) == "2c"
    end

    test "keeps device version when explicitly set" do
      org = build_org(snmp_version: "1")
      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_version) == "3"
    end

    test "keeps schema default version when no explicit change" do
      org = build_org(snmp_version: "1")
      changeset = device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      # Device schema default is "2c", resolver sees non-nil and keeps it
      assert get_field(result, :snmp_version) == "2c"
    end

    test "prefers site version over organization" do
      org = build_org(snmp_version: "1")
      site = build_site(snmp_version: "3")
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, site)

      assert get_field(result, :snmp_version) == "3"
    end

    test "falls back to organization when site version is nil" do
      org = build_org(snmp_version: "1")
      site = build_site(snmp_version: nil)
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, site)

      assert get_field(result, :snmp_version) == "1"
    end
  end

  # ── SNMP port resolution (via resolve_snmp_credentials/3) ──
  #
  # Device schema has `snmp_port: 161` default. Same behavior as version above.

  describe "resolve_snmp_credentials/3 snmp_port resolution" do
    test "resolves port from organization when field is nil" do
      org = build_org(snmp_port: 1161)
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_port) == 1161
    end

    test "defaults to 161 when neither site nor org has port" do
      org = build_org(snmp_port: nil)
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_port) == 161
    end

    test "keeps device port when explicitly set" do
      org = build_org(snmp_port: 1161)
      changeset = device_changeset(%{snmp_port: 9999})

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      assert get_field(result, :snmp_port) == 9999
    end

    test "keeps schema default port when no explicit change" do
      org = build_org(snmp_port: 1161)
      changeset = device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, nil)

      # Device schema default is 161, resolver sees non-nil and keeps it
      assert get_field(result, :snmp_port) == 161
    end

    test "prefers site port over organization" do
      org = build_org(snmp_port: 1161)
      site = build_site(snmp_port: 2222)
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, site)

      assert get_field(result, :snmp_port) == 2222
    end

    test "falls back to organization when site port is nil" do
      org = build_org(snmp_port: 1161)
      site = build_site(snmp_port: nil)
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmp_credentials(changeset, org, site)

      assert get_field(result, :snmp_port) == 1161
    end
  end

  # ── resolve_snmpv3_credentials/3 ──

  describe "resolve_snmpv3_credentials/3" do
    test "does nothing when snmp_version is not 3" do
      org = build_org(snmpv3_username: "v3user")
      changeset = device_changeset(%{snmp_version: "2c"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, nil)

      # Changeset is returned unmodified - SNMPv3 fields keep their schema defaults
      assert get_field(result, :snmpv3_username) == nil
      # Device schema default for snmpv3_credential_source is "site"
      assert get_field(result, :snmpv3_credential_source) == "site"
    end

    test "does nothing when snmp_version is nil (default Device changeset)" do
      org = build_org(snmpv3_username: "v3user")
      changeset = bare_device_changeset()

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, nil)

      assert get_field(result, :snmpv3_username) == nil
    end

    test "inherits SNMPv3 credentials from organization when device username is nil" do
      org =
        build_org(
          snmpv3_username: "org_v3user",
          snmpv3_security_level: "authPriv",
          snmpv3_auth_protocol: "SHA-512",
          snmpv3_auth_password: "orgauthpass",
          snmpv3_priv_protocol: "AES-256",
          snmpv3_priv_password: "orgprivpass"
        )

      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, nil)

      assert get_field(result, :snmpv3_username) == "org_v3user"
      assert get_field(result, :snmpv3_security_level) == "authPriv"
      assert get_field(result, :snmpv3_auth_protocol) == "SHA-512"
      assert get_field(result, :snmpv3_auth_password) == "orgauthpass"
      assert get_field(result, :snmpv3_priv_protocol) == "AES-256"
      assert get_field(result, :snmpv3_priv_password) == "orgprivpass"
      assert get_field(result, :snmpv3_credential_source) == "organization"
    end

    test "keeps device SNMPv3 credentials when username is set" do
      org = build_org(snmpv3_username: "org_v3user")

      changeset =
        device_changeset(%{snmp_version: "3", snmpv3_username: "device_v3user"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, nil)

      assert get_field(result, :snmpv3_username) == "device_v3user"
      assert get_field(result, :snmpv3_credential_source) == "device"
    end

    test "prefers site SNMPv3 credentials over organization" do
      org =
        build_org(
          snmpv3_username: "org_v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "orgauthpass"
        )

      site =
        build_site(
          snmpv3_username: "site_v3user",
          snmpv3_auth_protocol: "SHA-512",
          snmpv3_auth_password: "siteauthpass"
        )

      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, site)

      assert get_field(result, :snmpv3_username) == "site_v3user"
      assert get_field(result, :snmpv3_auth_protocol) == "SHA-512"
      assert get_field(result, :snmpv3_auth_password) == "siteauthpass"
      assert get_field(result, :snmpv3_credential_source) == "site"
    end

    test "falls back to organization when site SNMPv3 username is nil" do
      org = build_org(snmpv3_username: "org_v3user")
      site = build_site(snmpv3_username: nil)
      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, site)

      assert get_field(result, :snmpv3_username) == "org_v3user"
      assert get_field(result, :snmpv3_credential_source) == "organization"
    end

    test "uses default auth_protocol SHA-256 when hierarchy has nil" do
      org = build_org(snmpv3_username: "org_v3user", snmpv3_auth_protocol: nil)
      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, nil)

      assert get_field(result, :snmpv3_auth_protocol) == "SHA-256"
    end

    test "uses default priv_protocol AES when hierarchy has nil" do
      org = build_org(snmpv3_username: "org_v3user", snmpv3_priv_protocol: nil)
      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, nil)

      assert get_field(result, :snmpv3_priv_protocol) == "AES"
    end

    test "resolves individual SNMPv3 fields from mixed site/org sources" do
      org =
        build_org(
          snmpv3_username: "org_v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "orgauthpass",
          snmpv3_priv_protocol: "AES",
          snmpv3_priv_password: "orgprivpass"
        )

      # Site overrides only auth fields, priv fields fall back to org
      site =
        build_site(
          snmpv3_username: "site_v3user",
          snmpv3_auth_protocol: "SHA-512",
          snmpv3_auth_password: "siteauthpass",
          snmpv3_priv_protocol: nil,
          snmpv3_priv_password: nil
        )

      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, site)

      # Username and source come from site (since site username is set)
      assert get_field(result, :snmpv3_username) == "site_v3user"
      assert get_field(result, :snmpv3_credential_source) == "site"
      # Auth fields resolved from site
      assert get_field(result, :snmpv3_auth_protocol) == "SHA-512"
      assert get_field(result, :snmpv3_auth_password) == "siteauthpass"
      # Priv fields fall back to org
      assert get_field(result, :snmpv3_priv_protocol) == "AES"
      assert get_field(result, :snmpv3_priv_password) == "orgprivpass"
    end

    test "sets source to organization when both site and org username are nil" do
      org = build_org(snmpv3_username: nil)
      changeset = device_changeset(%{snmp_version: "3"})

      result = CredentialResolver.resolve_snmpv3_credentials(changeset, org, nil)

      assert get_field(result, :snmpv3_username) == nil
      assert get_field(result, :snmpv3_credential_source) == "organization"
    end
  end

  # ── resolve_mikrotik_credentials/3 ──

  describe "resolve_mikrotik_credentials/3" do
    test "inherits MikroTik credentials from organization when device username is nil" do
      org =
        build_org(
          mikrotik_username: "org_admin",
          mikrotik_password: "orgpass123",
          mikrotik_port: 8729,
          mikrotik_ssh_port: 22,
          mikrotik_use_ssl: true,
          mikrotik_enabled: true
        )

      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_username) == "org_admin"
      assert get_field(result, :mikrotik_password) == "orgpass123"
      assert get_field(result, :mikrotik_port) == 8729
      assert get_field(result, :mikrotik_ssh_port) == 22
      assert get_field(result, :mikrotik_use_ssl) == true
      assert get_field(result, :mikrotik_enabled) == true
      assert get_field(result, :mikrotik_credential_source) == "organization"
    end

    test "keeps device MikroTik credentials when username is set" do
      org = build_org(mikrotik_username: "org_admin")

      changeset = device_changeset(%{mikrotik_username: "device_admin"})

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_username) == "device_admin"
      assert get_field(result, :mikrotik_credential_source) == "device"
    end

    test "prefers site MikroTik credentials over organization" do
      org =
        build_org(
          mikrotik_username: "org_admin",
          mikrotik_password: "orgpass",
          mikrotik_port: 8729
        )

      site =
        build_site(
          mikrotik_username: "site_admin",
          mikrotik_password: "sitepass",
          mikrotik_port: 9999
        )

      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, site)

      assert get_field(result, :mikrotik_username) == "site_admin"
      assert get_field(result, :mikrotik_password) == "sitepass"
      assert get_field(result, :mikrotik_port) == 9999
      assert get_field(result, :mikrotik_credential_source) == "site"
    end

    test "falls back to organization when site MikroTik username is nil" do
      org = build_org(mikrotik_username: "org_admin")
      site = build_site(mikrotik_username: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, site)

      assert get_field(result, :mikrotik_username) == "org_admin"
      assert get_field(result, :mikrotik_credential_source) == "organization"
    end

    test "defaults mikrotik_port to 8729 when hierarchy has nil" do
      org = build_org(mikrotik_username: "org_admin", mikrotik_port: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_port) == 8729
    end

    test "defaults mikrotik_ssh_port to 22 when hierarchy has nil" do
      org = build_org(mikrotik_username: "org_admin", mikrotik_ssh_port: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_ssh_port) == 22
    end

    test "defaults mikrotik_use_ssl to true when hierarchy has nil" do
      org = build_org(mikrotik_username: "org_admin", mikrotik_use_ssl: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_use_ssl) == true
    end

    test "respects mikrotik_use_ssl false from hierarchy" do
      org = build_org(mikrotik_username: "org_admin", mikrotik_use_ssl: false)
      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_use_ssl) == false
    end

    test "defaults mikrotik_enabled to false when hierarchy has nil" do
      org = build_org(mikrotik_username: "org_admin", mikrotik_enabled: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_enabled) == false
    end

    test "resolves individual MikroTik fields from mixed site/org sources" do
      org =
        build_org(
          mikrotik_username: "org_admin",
          mikrotik_password: "orgpass",
          mikrotik_port: 8729,
          mikrotik_ssh_port: 22,
          mikrotik_use_ssl: true,
          mikrotik_enabled: true
        )

      # Site overrides username/password but not port/ssl
      site =
        build_site(
          mikrotik_username: "site_admin",
          mikrotik_password: "sitepass",
          mikrotik_port: nil,
          mikrotik_ssh_port: nil,
          mikrotik_use_ssl: nil,
          mikrotik_enabled: nil
        )

      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, site)

      # Username/password resolved from site
      assert get_field(result, :mikrotik_username) == "site_admin"
      assert get_field(result, :mikrotik_password) == "sitepass"
      assert get_field(result, :mikrotik_credential_source) == "site"
      # Port/ssl fall back to org
      assert get_field(result, :mikrotik_port) == 8729
      assert get_field(result, :mikrotik_ssh_port) == 22
      assert get_field(result, :mikrotik_use_ssl) == true
      assert get_field(result, :mikrotik_enabled) == true
    end

    test "sets source to organization when both site and org username are nil" do
      org = build_org(mikrotik_username: nil)
      changeset = device_changeset()

      result = CredentialResolver.resolve_mikrotik_credentials(changeset, org, nil)

      assert get_field(result, :mikrotik_username) == nil
      assert get_field(result, :mikrotik_credential_source) == "organization"
    end
  end

  # ── Full pipeline tests ──

  describe "full credential resolution pipeline" do
    test "resolves all credential types from organization using bare changeset" do
      org =
        build_org(
          snmp_community: "org_community",
          snmp_version: "3",
          snmp_port: 1161,
          snmpv3_username: "org_v3user",
          snmpv3_security_level: "authPriv",
          snmpv3_auth_protocol: "SHA-512",
          snmpv3_auth_password: "orgauthpass",
          snmpv3_priv_protocol: "AES-256",
          snmpv3_priv_password: "orgprivpass",
          mikrotik_username: "org_admin",
          mikrotik_password: "orgpass"
        )

      changeset = bare_device_changeset()

      result =
        changeset
        |> CredentialResolver.resolve_snmp_credentials(org, nil)
        |> CredentialResolver.resolve_snmpv3_credentials(org, nil)
        |> CredentialResolver.resolve_mikrotik_credentials(org, nil)

      # SNMP v2c
      assert get_field(result, :snmp_community) == "org_community"
      assert get_field(result, :snmp_community_source) == "organization"
      assert get_field(result, :snmp_version) == "3"
      assert get_field(result, :snmp_port) == 1161

      # SNMPv3 (version was resolved to "3" by resolve_snmp_credentials)
      assert get_field(result, :snmpv3_username) == "org_v3user"
      assert get_field(result, :snmpv3_credential_source) == "organization"

      # MikroTik
      assert get_field(result, :mikrotik_username) == "org_admin"
      assert get_field(result, :mikrotik_credential_source) == "organization"
    end

    test "device overrides take precedence over all hierarchy values" do
      org =
        build_org(
          snmp_community: "org_community",
          mikrotik_username: "org_admin"
        )

      site =
        build_site(
          snmp_community: "site_community",
          mikrotik_username: "site_admin"
        )

      changeset =
        device_changeset(%{
          snmp_community: "device_community",
          mikrotik_username: "device_admin"
        })

      result =
        changeset
        |> CredentialResolver.resolve_snmp_credentials(org, site)
        |> CredentialResolver.resolve_mikrotik_credentials(org, site)

      assert get_field(result, :snmp_community) == "device_community"
      assert get_field(result, :snmp_community_source) == "device"
      assert get_field(result, :mikrotik_username) == "device_admin"
      assert get_field(result, :mikrotik_credential_source) == "device"
    end

    test "site overrides org in full pipeline" do
      org =
        build_org(
          snmp_community: "org_community",
          mikrotik_username: "org_admin",
          mikrotik_password: "orgpass"
        )

      site =
        build_site(
          snmp_community: "site_community",
          mikrotik_username: "site_admin",
          mikrotik_password: "sitepass"
        )

      changeset = device_changeset()

      result =
        changeset
        |> CredentialResolver.resolve_snmp_credentials(org, site)
        |> CredentialResolver.resolve_mikrotik_credentials(org, site)

      assert get_field(result, :snmp_community) == "site_community"
      assert get_field(result, :snmp_community_source) == "site"
      assert get_field(result, :mikrotik_username) == "site_admin"
      assert get_field(result, :mikrotik_password) == "sitepass"
      assert get_field(result, :mikrotik_credential_source) == "site"
    end

    test "SNMPv3 resolution skipped when version is not 3" do
      org =
        build_org(
          snmpv3_username: "org_v3user",
          snmpv3_auth_password: "orgauthpass"
        )

      changeset = device_changeset()

      result =
        changeset
        |> CredentialResolver.resolve_snmp_credentials(org, nil)
        |> CredentialResolver.resolve_snmpv3_credentials(org, nil)

      # Version defaults to "2c" from schema, so SNMPv3 is skipped
      assert get_field(result, :snmp_version) == "2c"
      assert get_field(result, :snmpv3_username) == nil
    end
  end
end
