defmodule Towerops.Devices.DeviceSchemaTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures
  import Towerops.OrganizationsFixtures

  alias Towerops.Devices.Device

  defp create_org_and_site(_context) do
    user = user_fixture()
    org = organization_fixture(user.id)

    {:ok, site} =
      Towerops.Sites.create_site(%{
        name: "Test Site",
        organization_id: org.id
      })

    %{org: org, site: site}
  end

  defp valid_attrs(org, site) do
    %{
      ip_address: "192.168.1.1",
      organization_id: org.id,
      site_id: site.id,
      snmp_enabled: true,
      snmp_version: "2c",
      snmp_community: "public"
    }
  end

  describe "changeset/2 - required fields" do
    setup [:create_org_and_site]

    test "valid changeset with minimum required fields", %{org: org, site: site} do
      changeset = Device.changeset(%Device{}, valid_attrs(org, site))

      assert changeset.valid?
    end

    test "requires ip_address" do
      changeset = Device.changeset(%Device{}, %{organization_id: Ecto.UUID.generate()})

      refute changeset.valid?
      assert %{ip_address: ["can't be blank"]} = errors_on(changeset)
    end

    test "requires organization_id" do
      changeset = Device.changeset(%Device{}, %{ip_address: "192.168.1.1"})

      refute changeset.valid?
      assert %{organization_id: ["can't be blank"]} = errors_on(changeset)
    end
  end

  describe "validate_name/1" do
    setup [:create_org_and_site]

    test "name is not required when SNMP is enabled", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:name, nil)
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "name is required when SNMP is disabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.put(:snmp_enabled, false)
        |> Map.put(:name, nil)

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{name: ["can't be blank (required when SNMP is disabled)"]} = errors_on(changeset)
    end

    test "empty string name fails when SNMP is disabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.put(:snmp_enabled, false)
        |> Map.put(:name, "")

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{name: errors} = errors_on(changeset)
      assert "can't be blank (required when SNMP is disabled)" in errors
    end

    test "valid name when SNMP is disabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.put(:snmp_enabled, false)
        |> Map.put(:name, "My Router")

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "validates name minimum length of 2", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:name, "A")
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{name: [error]} = errors_on(changeset)
      assert error =~ "at least"
    end

    test "validates name maximum length of 200", %{org: org, site: site} do
      long_name = String.duplicate("a", 201)
      attrs = org |> valid_attrs(site) |> Map.put(:name, long_name)
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{name: [error]} = errors_on(changeset)
      assert error =~ "at most"
    end
  end

  describe "validate_ip_address/1" do
    setup [:create_org_and_site]

    test "accepts valid IPv4 address", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:ip_address, "10.0.0.1")
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts valid IPv6 address", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:ip_address, "::1")
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts full IPv6 address", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.put(:ip_address, "2001:0db8:85a3:0000:0000:8a2e:0370:7334")

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "rejects invalid IP address", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:ip_address, "not-an-ip")
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{ip_address: ["must be a valid IPv4 or IPv6 address"]} = errors_on(changeset)
    end

    test "rejects IP with out-of-range octet", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:ip_address, "192.168.1.256")
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{ip_address: ["must be a valid IPv4 or IPv6 address"]} = errors_on(changeset)
    end

    test "does not validate when ip_address is unchanged" do
      # When updating a device without changing ip_address, no validation runs
      existing = %Device{ip_address: "192.168.1.1", organization_id: Ecto.UUID.generate()}
      changeset = Device.changeset(existing, %{name: "Updated Name"})

      # No ip_address error even though we didn't re-validate
      refute Map.has_key?(errors_on(changeset), :ip_address)
    end
  end

  describe "validate_snmp/1 - SNMP version and port" do
    setup [:create_org_and_site]

    test "validates snmp_version inclusion when SNMP enabled", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_version, "4")
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{snmp_version: ["must be 1, 2c, or 3"]} = errors_on(changeset)
    end

    test "accepts snmp_version 1", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_version, "1")
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts snmp_version 2c", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_version, "2c")
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts snmp_version 3 with required username", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{snmp_version: "3", snmpv3_username: "v3user"})

      changeset = Device.changeset(%Device{snmpv3_auth_protocol: nil, snmpv3_priv_protocol: nil}, attrs)

      assert changeset.valid?
    end

    test "skips SNMP validation when SNMP disabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{snmp_enabled: false, snmp_version: "invalid", name: "My Device"})

      changeset = Device.changeset(%Device{}, attrs)

      # snmp_version is not validated when disabled
      refute Map.has_key?(errors_on(changeset), :snmp_version)
    end

    test "validates snmp_transport inclusion", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_transport, "invalid")
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{snmp_transport: ["must be udp or tcp"]} = errors_on(changeset)
    end

    test "accepts udp transport", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_transport, "udp")
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts tcp transport", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_transport, "tcp")
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end
  end

  describe "validate_snmp_port/1" do
    setup [:create_org_and_site]

    test "rejects port 0", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_port, 0)
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{snmp_port: _} = errors_on(changeset)
    end

    test "rejects port 65536", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_port, 65_536)
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{snmp_port: _} = errors_on(changeset)
    end

    test "accepts port 1", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_port, 1)
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts port 65535", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_port, 65_535)
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts default port 161", %{org: org, site: site} do
      changeset = Device.changeset(%Device{}, valid_attrs(org, site))

      assert changeset.valid?
      assert get_field(changeset, :snmp_port) == 161
    end
  end

  describe "validate_snmpv3_fields/1 - security level derivation" do
    setup [:create_org_and_site]

    test "derives noAuthNoPriv when no auth protocol", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{snmp_version: "3", snmpv3_username: "v3user"})

      changeset =
        Device.changeset(
          %Device{snmpv3_auth_protocol: nil, snmpv3_priv_protocol: nil},
          attrs
        )

      assert changeset.valid?
      assert get_field(changeset, :snmpv3_security_level) == "noAuthNoPriv"
    end

    test "derives authNoPriv when auth protocol set but no priv protocol", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA",
          snmpv3_auth_password: "longpassword"
        })

      changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)

      assert changeset.valid?
      assert get_field(changeset, :snmpv3_security_level) == "authNoPriv"
    end

    test "derives authPriv when both auth and priv protocols set", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "longpassword",
          snmpv3_priv_protocol: "AES",
          snmpv3_priv_password: "longpassword"
        })

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
      assert get_field(changeset, :snmpv3_security_level) == "authPriv"
    end

    test "treats empty auth protocol as noAuthNoPriv", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: ""
        })

      changeset =
        Device.changeset(
          %Device{snmpv3_auth_protocol: nil, snmpv3_priv_protocol: nil},
          attrs
        )

      assert changeset.valid?
      assert get_field(changeset, :snmpv3_security_level) == "noAuthNoPriv"
    end
  end

  describe "validate_snmpv3_fields/1 - username requirement" do
    setup [:create_org_and_site]

    test "requires snmpv3_username when snmp_version is 3", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_version, "3")

      changeset =
        Device.changeset(
          %Device{snmpv3_auth_protocol: nil, snmpv3_priv_protocol: nil},
          attrs
        )

      refute changeset.valid?
      assert %{snmpv3_username: ["can't be blank"]} = errors_on(changeset)
    end

    test "does not require snmpv3_username for version 2c", %{org: org, site: site} do
      changeset = Device.changeset(%Device{}, valid_attrs(org, site))

      assert changeset.valid?
      refute Map.has_key?(errors_on(changeset), :snmpv3_username)
    end
  end

  describe "validate_snmpv3_auth_required/1" do
    setup [:create_org_and_site]

    test "requires auth_password when auth_protocol is set", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256"
        })

      changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)

      refute changeset.valid?
      assert %{snmpv3_auth_password: ["required when auth protocol is selected"]} = errors_on(changeset)
    end

    test "validates auth_protocol inclusion" do
      # Use a non-DB-touching changeset path: no site_id
      attrs = %{
        ip_address: "10.0.0.1",
        organization_id: Ecto.UUID.generate(),
        snmp_enabled: true,
        snmp_version: "3",
        snmpv3_username: "v3user",
        snmpv3_auth_protocol: "INVALID",
        snmpv3_auth_password: "longpassword"
      }

      changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)

      refute changeset.valid?
      assert %{snmpv3_auth_protocol: _} = errors_on(changeset)
    end

    test "accepts all valid auth protocols", %{org: org, site: site} do
      protocols = ["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]

      for protocol <- protocols do
        attrs =
          org
          |> valid_attrs(site)
          |> Map.merge(%{
            snmp_version: "3",
            snmpv3_username: "v3user",
            snmpv3_auth_protocol: protocol,
            snmpv3_auth_password: "longpassword"
          })

        changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)
        assert changeset.valid?, "Expected #{protocol} to be accepted"
      end
    end

    test "no auth validation when auth_protocol is nil", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{snmp_version: "3", snmpv3_username: "v3user"})

      changeset =
        Device.changeset(
          %Device{snmpv3_auth_protocol: nil, snmpv3_priv_protocol: nil},
          attrs
        )

      assert changeset.valid?
      refute Map.has_key?(errors_on(changeset), :snmpv3_auth_password)
    end
  end

  describe "validate_snmpv3_priv_required/1" do
    setup [:create_org_and_site]

    test "requires priv_password when priv_protocol is set", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "longpassword",
          snmpv3_priv_protocol: "AES"
        })

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{snmpv3_priv_password: ["required when privacy protocol is selected"]} = errors_on(changeset)
    end

    test "validates priv_protocol inclusion" do
      attrs = %{
        ip_address: "10.0.0.1",
        organization_id: Ecto.UUID.generate(),
        snmp_enabled: true,
        snmp_version: "3",
        snmpv3_username: "v3user",
        snmpv3_auth_protocol: "SHA-256",
        snmpv3_auth_password: "longpassword",
        snmpv3_priv_protocol: "INVALID",
        snmpv3_priv_password: "longpassword"
      }

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{snmpv3_priv_protocol: _} = errors_on(changeset)
    end

    test "accepts all valid priv protocols", %{org: org, site: site} do
      protocols = ["DES", "AES", "AES-192", "AES-256", "AES-256-C"]

      for protocol <- protocols do
        attrs =
          org
          |> valid_attrs(site)
          |> Map.merge(%{
            snmp_version: "3",
            snmpv3_username: "v3user",
            snmpv3_auth_protocol: "SHA-256",
            snmpv3_auth_password: "longpassword",
            snmpv3_priv_protocol: protocol,
            snmpv3_priv_password: "longpassword"
          })

        changeset = Device.changeset(%Device{}, attrs)
        assert changeset.valid?, "Expected #{protocol} to be accepted"
      end
    end

    test "no priv validation when priv_protocol is nil", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "longpassword"
        })

      changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)

      assert changeset.valid?
      refute Map.has_key?(errors_on(changeset), :snmpv3_priv_password)
    end
  end

  describe "validate_password_length/3" do
    setup [:create_org_and_site]

    test "rejects auth_password shorter than 8 characters", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "short"
        })

      changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)

      refute changeset.valid?
      assert %{snmpv3_auth_password: errors} = errors_on(changeset)
      assert "must be at least 8 characters" in errors
    end

    test "rejects priv_password shorter than 8 characters", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "longpassword",
          snmpv3_priv_protocol: "AES",
          snmpv3_priv_password: "short"
        })

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{snmpv3_priv_password: errors} = errors_on(changeset)
      assert "must be at least 8 characters" in errors
    end

    test "accepts auth_password of exactly 8 characters", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "12345678"
        })

      changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)

      assert changeset.valid?
    end

    test "accepts priv_password of exactly 8 characters", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "v3user",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "longpassword",
          snmpv3_priv_protocol: "AES",
          snmpv3_priv_password: "12345678"
        })

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "does not validate password length when password is not changed", %{org: org, site: site} do
      # Existing device with snmpv3 credentials, update only name
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "3",
        snmpv3_username: "v3user",
        snmpv3_auth_protocol: "SHA-256",
        snmpv3_auth_password: "old",
        snmpv3_priv_protocol: nil,
        snmpv3_security_level: "authNoPriv"
      }

      changeset = Device.changeset(existing, %{name: "Updated Name"})

      # Password length not re-validated since it was not changed
      refute Map.has_key?(errors_on(changeset), :snmpv3_auth_password)
    end
  end

  describe "validate_mikrotik/1" do
    setup [:create_org_and_site]

    test "validates mikrotik_port range when enabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{mikrotik_enabled: true, mikrotik_port: 0})

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{mikrotik_port: _} = errors_on(changeset)
    end

    test "rejects mikrotik_port 65536 when enabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{mikrotik_enabled: true, mikrotik_port: 65_536})

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{mikrotik_port: _} = errors_on(changeset)
    end

    test "accepts valid mikrotik_port when enabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{mikrotik_enabled: true, mikrotik_port: 8729})

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "skips mikrotik validation when disabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{mikrotik_enabled: false, mikrotik_port: 0})

      changeset = Device.changeset(%Device{}, attrs)

      # No mikrotik_port error because validation is skipped
      refute Map.has_key?(errors_on(changeset), :mikrotik_port)
    end

    test "skips mikrotik validation when mikrotik_enabled is nil", %{org: org, site: site} do
      attrs = valid_attrs(org, site)
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end
  end

  describe "validate_mikrotik_ssl_for_cloud/1" do
    setup [:create_org_and_site]

    test "allows mikrotik_use_ssl to be false when enabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{mikrotik_enabled: true, mikrotik_use_ssl: false})

      changeset = Device.changeset(%Device{}, attrs)

      # SSL=false is allowed at changeset level (cloud check is context-level)
      assert changeset.valid?
    end

    test "allows mikrotik_use_ssl to be true when enabled", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{mikrotik_enabled: true, mikrotik_use_ssl: true})

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "allows mikrotik_use_ssl to be nil (inherit from site)", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{mikrotik_enabled: true, mikrotik_use_ssl: nil})

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end
  end

  describe "update_snmpv3_credential_source/1" do
    setup [:create_org_and_site]

    test "sets source to device when username is explicitly set", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{snmp_version: "3", snmpv3_username: "myuser"})

      changeset =
        Device.changeset(
          %Device{snmpv3_auth_protocol: nil, snmpv3_priv_protocol: nil},
          attrs
        )

      assert get_field(changeset, :snmpv3_credential_source) == "device"
    end

    test "sets source to site when username is cleared to empty string", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmpv3_username: "old_user",
        snmpv3_credential_source: "device",
        snmpv3_auth_protocol: nil,
        snmpv3_priv_protocol: nil
      }

      changeset = Device.changeset(existing, %{snmpv3_username: ""})

      assert get_field(changeset, :snmpv3_credential_source) == "site"
      assert get_field(changeset, :snmpv3_username) == nil
    end

    test "resets source to site when username is nil and source was device", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmpv3_username: nil,
        snmpv3_credential_source: "device",
        snmpv3_auth_protocol: nil,
        snmpv3_priv_protocol: nil
      }

      # Trigger changeset with unrelated field change
      changeset = Device.changeset(existing, %{name: "Updated"})

      assert get_field(changeset, :snmpv3_credential_source) == "site"
    end

    test "keeps existing source when no username change", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmpv3_username: "existing_user",
        snmpv3_credential_source: "device",
        snmpv3_auth_protocol: nil,
        snmpv3_priv_protocol: nil
      }

      changeset = Device.changeset(existing, %{name: "Updated"})

      assert get_field(changeset, :snmpv3_credential_source) == "device"
    end
  end

  describe "update_mikrotik_credential_source/1" do
    setup [:create_org_and_site]

    test "sets source to device when username is explicitly set", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.put(:mikrotik_username, "admin")

      changeset = Device.changeset(%Device{}, attrs)

      assert get_field(changeset, :mikrotik_credential_source) == "device"
    end

    test "sets source to site when username is cleared to empty string", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        mikrotik_username: "old_user",
        mikrotik_credential_source: "device",
        snmpv3_auth_protocol: nil,
        snmpv3_priv_protocol: nil
      }

      changeset = Device.changeset(existing, %{mikrotik_username: ""})

      assert get_field(changeset, :mikrotik_credential_source) == "site"
      assert get_field(changeset, :mikrotik_username) == nil
    end

    test "resets source to site when username is nil and source was device", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        mikrotik_username: nil,
        mikrotik_credential_source: "device",
        snmpv3_auth_protocol: nil,
        snmpv3_priv_protocol: nil
      }

      changeset = Device.changeset(existing, %{name: "Updated"})

      assert get_field(changeset, :mikrotik_credential_source) == "site"
    end

    test "keeps existing source when no username change", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        mikrotik_username: "admin",
        mikrotik_credential_source: "device",
        snmpv3_auth_protocol: nil,
        snmpv3_priv_protocol: nil
      }

      changeset = Device.changeset(existing, %{name: "Updated"})

      assert get_field(changeset, :mikrotik_credential_source) == "device"
    end
  end

  describe "update_community_source/1" do
    setup [:create_org_and_site]

    test "sets source to device when community is explicitly set", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_community, "private")
      changeset = Device.changeset(%Device{}, attrs)

      assert get_field(changeset, :snmp_community_source) == "device"
    end

    test "cast normalizes empty community to nil, so source is unchanged", %{org: org, site: site} do
      # Ecto cast converts "" to nil for string fields, so get_change returns nil
      # and update_community_source treats it as "no change"
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public",
        snmp_community_source: "device"
      }

      changeset = Device.changeset(existing, %{snmp_community: ""})

      assert get_field(changeset, :snmp_community_source) == "device"
    end

    test "sets source to site when community changes to a new non-nil value", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_community, "new_community")
      changeset = Device.changeset(%Device{}, attrs)

      assert get_field(changeset, :snmp_community_source) == "device"
    end

    test "keeps existing source when community is not changed", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public",
        snmp_community_source: "device"
      }

      changeset = Device.changeset(existing, %{name: "Updated"})

      assert get_field(changeset, :snmp_community_source) == "device"
    end
  end

  describe "update_transport_source/1" do
    setup [:create_org_and_site]

    test "sets source to device when transport is explicitly set", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:snmp_transport, "tcp")
      changeset = Device.changeset(%Device{}, attrs)

      assert get_field(changeset, :snmp_transport_source) == "device"
    end

    test "cast normalizes empty transport to nil, so source is unchanged", %{org: org, site: site} do
      # Ecto cast converts "" to nil for string fields, so get_change returns nil
      # and update_transport_source treats it as "no change"
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_transport: "tcp",
        snmp_transport_source: "device"
      }

      changeset = Device.changeset(existing, %{snmp_transport: ""})

      assert get_field(changeset, :snmp_transport_source) == "device"
    end

    test "keeps existing source when transport is not changed", %{org: org, site: site} do
      existing = %Device{
        ip_address: "10.0.0.1",
        organization_id: org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_transport: "tcp",
        snmp_transport_source: "device"
      }

      changeset = Device.changeset(existing, %{name: "Updated"})

      assert get_field(changeset, :snmp_transport_source) == "device"
    end
  end

  describe "validate_site_belongs_to_organization/1" do
    setup [:create_org_and_site]

    test "accepts device with site belonging to same org", %{org: org, site: site} do
      changeset = Device.changeset(%Device{}, valid_attrs(org, site))

      assert changeset.valid?
    end

    test "rejects device with non-existent site", %{org: org} do
      attrs = %{
        ip_address: "192.168.1.1",
        organization_id: org.id,
        site_id: Ecto.UUID.generate(),
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public"
      }

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{site_id: ["does not exist"]} = errors_on(changeset)
    end

    test "rejects device with site from different org", %{site: site} do
      other_user = user_fixture()
      other_org = organization_fixture(other_user.id)

      attrs = %{
        ip_address: "192.168.1.1",
        organization_id: other_org.id,
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public"
      }

      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{site_id: ["must belong to the same organization"]} = errors_on(changeset)
    end

    test "allows device with no site_id" do
      attrs = %{
        ip_address: "192.168.1.1",
        organization_id: Ecto.UUID.generate(),
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public"
      }

      changeset = Device.changeset(%Device{}, attrs)

      # No site_id error (site is optional)
      refute Map.has_key?(errors_on(changeset), :site_id)
    end
  end

  describe "changeset/2 - check_interval_seconds validation" do
    setup [:create_org_and_site]

    test "rejects check_interval_seconds of 0", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:check_interval_seconds, 0)
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{check_interval_seconds: _} = errors_on(changeset)
    end

    test "rejects check_interval_seconds greater than 3600", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:check_interval_seconds, 3601)
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{check_interval_seconds: _} = errors_on(changeset)
    end

    test "accepts check_interval_seconds of 1", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:check_interval_seconds, 1)
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "accepts check_interval_seconds of 3600", %{org: org, site: site} do
      attrs = org |> valid_attrs(site) |> Map.put(:check_interval_seconds, 3600)
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "defaults check_interval_seconds to 300", %{org: org, site: site} do
      changeset = Device.changeset(%Device{}, valid_attrs(org, site))

      assert get_field(changeset, :check_interval_seconds) == 300
    end
  end

  describe "changeset/2 - description validation" do
    setup [:create_org_and_site]

    test "accepts description up to 1000 characters", %{org: org, site: site} do
      desc = String.duplicate("a", 1000)
      attrs = org |> valid_attrs(site) |> Map.put(:description, desc)
      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
    end

    test "rejects description over 1000 characters", %{org: org, site: site} do
      desc = String.duplicate("a", 1001)
      attrs = org |> valid_attrs(site) |> Map.put(:description, desc)
      changeset = Device.changeset(%Device{}, attrs)

      refute changeset.valid?
      assert %{description: [error]} = errors_on(changeset)
      assert error =~ "at most"
    end
  end

  describe "changeset/2 - full SNMPv3 authPriv config" do
    setup [:create_org_and_site]

    test "valid full SNMPv3 authPriv configuration", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "snmpv3admin",
          snmpv3_auth_protocol: "SHA-512",
          snmpv3_auth_password: "authpass123456",
          snmpv3_priv_protocol: "AES-256",
          snmpv3_priv_password: "privpass123456"
        })

      changeset = Device.changeset(%Device{}, attrs)

      assert changeset.valid?
      assert get_field(changeset, :snmpv3_security_level) == "authPriv"
      assert get_field(changeset, :snmpv3_credential_source) == "device"
    end

    test "valid full SNMPv3 authNoPriv configuration", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{
          snmp_version: "3",
          snmpv3_username: "snmpv3admin",
          snmpv3_auth_protocol: "MD5",
          snmpv3_auth_password: "authpass123456"
        })

      changeset = Device.changeset(%Device{snmpv3_priv_protocol: nil}, attrs)

      assert changeset.valid?
      assert get_field(changeset, :snmpv3_security_level) == "authNoPriv"
    end

    test "valid full SNMPv3 noAuthNoPriv configuration", %{org: org, site: site} do
      attrs =
        org
        |> valid_attrs(site)
        |> Map.merge(%{snmp_version: "3", snmpv3_username: "snmpv3admin"})

      changeset =
        Device.changeset(
          %Device{snmpv3_auth_protocol: nil, snmpv3_priv_protocol: nil},
          attrs
        )

      assert changeset.valid?
      assert get_field(changeset, :snmpv3_security_level) == "noAuthNoPriv"
    end
  end
end
