defmodule SnmpKit.SnmpLib.SecurityTest do
  use ExUnit.Case, async: false

  alias SnmpKit.SnmpLib.Security

  @moduletag :snmpv3
  doctest Security

  @moduletag :security
  @moduletag :phase5

  describe "Security.create_user/2" do
    test "creates user with authentication only" do
      config = [
        auth_protocol: :sha256,
        auth_password: "test_password_123",
        engine_id: "test_engine_001"
      ]

      assert {:ok, user} = Security.create_user("test_user", config)
      assert user.security_name == "test_user"
      assert user.auth_protocol == :sha256
      assert user.priv_protocol == :none
      assert byte_size(user.auth_key) > 0
      assert user.priv_key == <<>>
      assert user.engine_id == "test_engine_001"
    end

    test "creates user with authentication and privacy" do
      config = [
        auth_protocol: :sha256,
        auth_password: "auth_password_456",
        priv_protocol: :aes256,
        priv_password: "priv_password_789",
        engine_id: "secure_engine_002"
      ]

      assert {:ok, user} = Security.create_user("secure_user", config)
      assert user.security_name == "secure_user"
      assert user.auth_protocol == :sha256
      assert user.priv_protocol == :aes256
      assert byte_size(user.auth_key) > 0
      assert byte_size(user.priv_key) > 0
      assert user.engine_id == "secure_engine_002"
    end

    test "rejects invalid configurations" do
      # Missing auth password when auth protocol specified
      config = [
        auth_protocol: :sha256,
        engine_id: "test_engine"
      ]

      assert {:error, :missing_auth_password} = Security.create_user("user", config)

      # Privacy without authentication
      config = [
        priv_protocol: :aes128,
        priv_password: "priv_pass",
        engine_id: "test_engine"
      ]

      assert {:error, :priv_requires_auth} = Security.create_user("user", config)

      # Missing engine ID
      config = [
        auth_protocol: :sha256,
        auth_password: "auth_pass"
      ]

      assert {:error, :missing_engine_id} = Security.create_user("user", config)

      # Unsupported protocol
      config = [
        auth_protocol: :invalid_proto,
        auth_password: "auth_pass",
        engine_id: "test_engine"
      ]

      assert {:error, :invalid_auth_protocol} = Security.create_user("user", config)
    end
  end

  describe "Security.get_security_level/1" do
    test "determines correct security levels" do
      # No auth, no priv
      user = %{auth_protocol: :none, priv_protocol: :none}
      assert Security.get_security_level(user) == :no_auth_no_priv

      # Auth only
      user = %{auth_protocol: :sha256, priv_protocol: :none}
      assert Security.get_security_level(user) == :auth_no_priv

      # Auth and priv
      user = %{auth_protocol: :sha256, priv_protocol: :aes256}
      assert Security.get_security_level(user) == :auth_priv
    end
  end

  describe "Security.authenticate_message/2" do
    test "authenticates message with SHA-256" do
      {:ok, user} =
        Security.create_user("auth_user",
          auth_protocol: :sha256,
          auth_password: "secure_auth_password",
          engine_id: "auth_engine_123"
        )

      message = "test SNMP message data"

      assert {:ok, auth_params} = Security.authenticate_message(user, message)
      assert is_binary(auth_params)
      assert byte_size(auth_params) > 0
    end

    test "returns empty auth params for no auth" do
      user = %{auth_protocol: :none}
      message = "test message"

      assert {:ok, <<>>} = Security.authenticate_message(user, message)
    end
  end

  describe "Security.verify_authentication/3" do
    test "verifies valid authentication" do
      {:ok, user} =
        Security.create_user("verify_user",
          auth_protocol: :sha256,
          auth_password: "verification_password",
          engine_id: "verify_engine_456"
        )

      message = "message to verify"
      {:ok, auth_params} = Security.authenticate_message(user, message)

      assert :ok = Security.verify_authentication(user, message, auth_params)
    end

    test "rejects invalid authentication" do
      {:ok, user} =
        Security.create_user("verify_user",
          auth_protocol: :sha256,
          auth_password: "verification_password",
          engine_id: "verify_engine_456"
        )

      message = "message to verify"
      fake_auth_params = :crypto.strong_rand_bytes(12)

      assert {:error, :authentication_mismatch} =
               Security.verify_authentication(user, message, fake_auth_params)
    end
  end

  describe "Security.encrypt_message/2" do
    test "encrypts message with AES-256" do
      {:ok, user} =
        Security.create_user("encrypt_user",
          auth_protocol: :sha256,
          auth_password: "auth_for_encryption",
          priv_protocol: :aes256,
          priv_password: "privacy_password_strong",
          engine_id: "encrypt_engine_789"
        )

      plaintext = "confidential SNMP data that needs encryption"

      assert {:ok, {ciphertext, priv_params}} = Security.encrypt_message(user, plaintext)
      assert is_binary(ciphertext)
      assert is_binary(priv_params)
      assert ciphertext != plaintext
      assert byte_size(priv_params) > 0
    end

    test "returns plaintext for no privacy" do
      user = %{priv_protocol: :none}
      plaintext = "unencrypted message"

      assert {:ok, {^plaintext, <<>>}} = Security.encrypt_message(user, plaintext)
    end
  end

  describe "Security.decrypt_message/3" do
    test "decrypts AES-256 encrypted message" do
      {:ok, user} =
        Security.create_user("decrypt_user",
          auth_protocol: :sha256,
          auth_password: "auth_for_decryption",
          priv_protocol: :aes256,
          priv_password: "strong_privacy_password",
          engine_id: "decrypt_engine_abc"
        )

      original_plaintext = "secret data that was encrypted"

      # Encrypt first
      {:ok, {ciphertext, priv_params}} = Security.encrypt_message(user, original_plaintext)

      # Then decrypt
      assert {:ok, decrypted_plaintext} = Security.decrypt_message(user, ciphertext, priv_params)
      assert decrypted_plaintext == original_plaintext
    end

    test "handles decryption with wrong key" do
      # Create two users with different keys
      {:ok, user1} =
        Security.create_user("user1",
          auth_protocol: :sha256,
          auth_password: "password1",
          priv_protocol: :aes256,
          priv_password: "privpass1",
          engine_id: "engine1"
        )

      {:ok, user2} =
        Security.create_user("user2",
          auth_protocol: :sha256,
          auth_password: "password2",
          priv_protocol: :aes256,
          priv_password: "privpass2",
          engine_id: "engine2"
        )

      plaintext = "test encryption data"

      # Encrypt with user1
      {:ok, {ciphertext, priv_params}} = Security.encrypt_message(user1, plaintext)

      # Try to decrypt with user2 (should fail due to wrong key)
      # The wrong key will typically result in invalid padding after decryption
      result = Security.decrypt_message(user2, ciphertext, priv_params)

      case result do
        {:error, _reason} ->
          # Most common case - padding validation fails with wrong key
          :ok

        {:ok, decrypted_data} ->
          # In rare cases decryption might succeed with garbage data
          # Ensure it's different from the original
          assert decrypted_data != plaintext,
                 "Decryption with wrong key should produce different data"
      end
    end
  end

  describe "Security.generate_engine_id/1" do
    test "generates valid engine IDs" do
      identifier = "test.device.local"
      engine_id = Security.generate_engine_id(identifier)

      assert is_binary(engine_id)
      assert byte_size(engine_id) >= 5
      assert byte_size(engine_id) <= 32

      # Should be deterministic for same identifier
      engine_id2 = Security.generate_engine_id(identifier)
      # Actually includes timestamp, so different
      assert engine_id != engine_id2
    end

    test "generates different engine IDs for different identifiers" do
      engine_id1 = Security.generate_engine_id("device1")
      engine_id2 = Security.generate_engine_id("device2")

      assert engine_id1 != engine_id2
    end
  end

  describe "Security.build_security_params/3" do
    test "builds valid security parameters" do
      {:ok, user} =
        Security.create_user("param_user",
          auth_protocol: :sha256,
          auth_password: "param_password",
          engine_id: "param_engine_def"
        )

      auth_params = :crypto.strong_rand_bytes(16)
      priv_params = :crypto.strong_rand_bytes(16)

      params = Security.build_security_params(user, auth_params, priv_params)

      assert params.authoritative_engine_id == user.engine_id
      assert params.user_name == user.security_name
      assert params.authentication_parameters == auth_params
      assert params.privacy_parameters == priv_params
      assert is_integer(params.authoritative_engine_boots)
      assert is_integer(params.authoritative_engine_time)
    end
  end

  describe "Security.update_engine_time/3" do
    test "updates engine time correctly" do
      {:ok, user} =
        Security.create_user("time_user",
          auth_protocol: :sha256,
          auth_password: "time_password",
          engine_id: "time_engine_ghi"
        )

      new_boots = 5
      new_time = 123_456

      updated_user = Security.update_engine_time(user, new_boots, new_time)

      assert updated_user.engine_boots == new_boots
      assert updated_user.engine_time == new_time
      assert updated_user.security_name == user.security_name
      assert updated_user.auth_key == user.auth_key
    end
  end

  describe "Security.validate_user/3" do
    test "validates correct passwords" do
      auth_password = "correct_auth_password"
      priv_password = "correct_priv_password"

      {:ok, user} =
        Security.create_user("validate_user",
          auth_protocol: :sha256,
          auth_password: auth_password,
          priv_protocol: :aes256,
          priv_password: priv_password,
          engine_id: "validate_engine_jkl"
        )

      assert :ok = Security.validate_user(user, auth_password, priv_password)
    end

    test "rejects incorrect passwords" do
      {:ok, user} =
        Security.create_user("validate_user",
          auth_protocol: :sha256,
          auth_password: "correct_auth",
          priv_protocol: :aes256,
          priv_password: "correct_priv",
          engine_id: "validate_engine_mno"
        )

      assert {:error, :invalid_auth_password} =
               Security.validate_user(user, "wrong_auth", "correct_priv")

      assert {:error, :invalid_priv_password} =
               Security.validate_user(user, "correct_auth", "wrong_priv")
    end
  end

  describe "Security.info/0" do
    test "returns comprehensive security information" do
      info = Security.info()

      assert info.version == "5.1.0"
      assert info.phase == "5.1A - Security Foundation"
      assert is_list(info.supported_auth_protocols)
      assert is_list(info.supported_priv_protocols)
      assert is_list(info.rfc_compliance)
      assert is_list(info.security_levels)
      assert is_list(info.features)

      # Check that modern protocols are supported
      assert :sha256 in info.supported_auth_protocols
      assert :aes256 in info.supported_priv_protocols
      assert :auth_priv in info.security_levels
    end
  end

  describe "integration tests" do
    test "complete SNMPv3 security workflow" do
      # Step 1: Create secure user
      {:ok, user} =
        Security.create_user("workflow_user",
          auth_protocol: :sha256,
          auth_password: "strong_authentication_password",
          priv_protocol: :aes256,
          priv_password: "very_strong_privacy_password",
          engine_id: "workflow_engine_test"
        )

      # Step 2: Verify security level
      assert Security.get_security_level(user) == :auth_priv

      # Step 3: Authenticate a message
      test_message = "This is a complete SNMPv3 test message with authentication and privacy"
      {:ok, auth_params} = Security.authenticate_message(user, test_message)

      # Step 4: Encrypt the message
      {:ok, {encrypted_message, priv_params}} = Security.encrypt_message(user, test_message)

      # Step 5: Build security parameters
      security_params = Security.build_security_params(user, auth_params, priv_params)

      # Step 6: Verify authentication (simulate receiving side)
      assert :ok = Security.verify_authentication(user, test_message, auth_params)

      # Step 7: Decrypt the message (simulate receiving side)
      {:ok, decrypted_message} = Security.decrypt_message(user, encrypted_message, priv_params)

      # Step 8: Verify complete round-trip
      assert decrypted_message == test_message
      assert security_params.user_name == user.security_name
      assert security_params.authoritative_engine_id == user.engine_id
    end

    test "supports multiple security levels in same system" do
      # No auth user
      {:ok, no_auth_user} =
        Security.create_user("no_auth",
          auth_protocol: :none,
          engine_id: "multi_engine_test"
        )

      # Auth only user
      {:ok, auth_user} =
        Security.create_user("auth_only",
          auth_protocol: :sha256,
          auth_password: "auth_password",
          engine_id: "multi_engine_test"
        )

      # Auth + priv user
      {:ok, full_user} =
        Security.create_user("full_security",
          auth_protocol: :sha256,
          auth_password: "auth_password",
          priv_protocol: :aes256,
          priv_password: "priv_password",
          engine_id: "multi_engine_test"
        )

      # Verify different security levels
      assert Security.get_security_level(no_auth_user) == :no_auth_no_priv
      assert Security.get_security_level(auth_user) == :auth_no_priv
      assert Security.get_security_level(full_user) == :auth_priv

      # All should work with appropriate operations
      message = "test message for all users"

      # No auth user
      {:ok, <<>>} = Security.authenticate_message(no_auth_user, message)
      {:ok, {^message, <<>>}} = Security.encrypt_message(no_auth_user, message)

      # Auth user
      {:ok, auth_params} = Security.authenticate_message(auth_user, message)
      assert byte_size(auth_params) > 0
      {:ok, {^message, <<>>}} = Security.encrypt_message(auth_user, message)

      # Full user
      {:ok, auth_params} = Security.authenticate_message(full_user, message)
      {:ok, {encrypted, priv_params}} = Security.encrypt_message(full_user, message)
      assert byte_size(auth_params) > 0
      assert byte_size(priv_params) > 0
      assert encrypted != message
    end
  end

  describe "error handling and edge cases" do
    test "handles empty and invalid inputs gracefully" do
      # Empty passwords
      config = [
        auth_protocol: :sha256,
        auth_password: "",
        engine_id: "test_engine"
      ]

      assert {:error, _reason} = Security.create_user("test", config)

      # Invalid engine ID
      config = [
        auth_protocol: :sha256,
        auth_password: "valid_password",
        engine_id: ""
      ]

      assert {:error, _reason} = Security.create_user("test", config)
    end

    test "handles protocol edge cases" do
      # All supported auth protocols should work
      auth_protocols = [:md5, :sha1, :sha256, :sha384, :sha512]

      for protocol <- auth_protocols do
        config = [
          auth_protocol: protocol,
          auth_password: "test_password_for_#{protocol}",
          engine_id: "edge_case_engine"
        ]

        assert {:ok, _user} = Security.create_user("user_#{protocol}", config)
      end

      # All supported priv protocols should work
      priv_protocols = [:des, :aes128, :aes192, :aes256]

      for protocol <- priv_protocols do
        config = [
          auth_protocol: :sha256,
          auth_password: "auth_password",
          priv_protocol: protocol,
          priv_password: "priv_password_for_#{protocol}",
          engine_id: "edge_case_engine"
        ]

        assert {:ok, _user} = Security.create_user("user_#{protocol}", config)
      end
    end
  end
end
