defmodule Raxol.ACP.Wallet.SCA.ProvisioningTest do
  use ExUnit.Case, async: true

  alias Raxol.ACP.ABI
  alias Raxol.ACP.Wallet.SCA.{ModularAccount, Provisioning}

  @owner "0x9a96e767bfcce8e80370be00821ed5ba283d4a17"

  describe "predict_address/2" do
    # Reference addresses generated by Account Kit's
    # predictModularAccountV2Address (SMA) replicated with viem 2.51.3:
    #
    #   getContractAddress({ from: factory, opcode: "CREATE2",
    #     salt: keccak256(encodePacked(["address","uint256","uint32"],
    #       [owner, salt, 0xffffffff])),
    #     bytecode: proxyWithImmutableArgs(smaBytecode, owner) })
    test "matches the Account Kit / viem counterfactual SMA address (salt 0)" do
      assert Provisioning.predict_address(@owner, 0) ==
               "0xcfba4d9139768f108a648efe148de5474d258765"
    end

    test "matches for a nonzero salt" do
      assert Provisioning.predict_address(@owner, 7) ==
               "0xf13663d3642a64644015f3387ab9f30809de8844"
    end

    test "defaults salt to 0" do
      assert Provisioning.predict_address(@owner) == Provisioning.predict_address(@owner, 0)
    end

    test "is deterministic and 0x-prefixed 20-byte hex" do
      addr = Provisioning.predict_address(@owner)
      assert String.starts_with?(addr, "0x")
      assert byte_size(addr) == 42
      assert addr == Provisioning.predict_address(@owner)
    end

    test "different owners yield different addresses" do
      other = "0x" <> String.duplicate("11", 20)
      assert Provisioning.predict_address(@owner) != Provisioning.predict_address(other)
    end
  end

  describe "deploy_init_code/2" do
    test "is factory address ++ createSemiModularAccount(owner, salt) calldata" do
      init_code = Provisioning.deploy_init_code(@owner, 0)

      <<factory::binary-size(20), call_data::binary>> = init_code

      assert "0x" <> Base.encode16(factory, case: :lower) ==
               String.downcase(ModularAccount.factory_address())

      expected =
        ABI.encode_call("createSemiModularAccount(address,uint256)", [
          {"address", @owner},
          {"uint256", 0}
        ])

      assert call_data == expected
    end

    test "the factory call selector matches createSemiModularAccount" do
      <<_factory::binary-size(20), selector::binary-size(4), _rest::binary>> =
        Provisioning.deploy_init_code(@owner, 0)

      expected = binary_part(ExKeccak.hash_256("createSemiModularAccount(address,uint256)"), 0, 4)
      assert selector == expected
    end

    test "encodes the salt" do
      <<_factory::binary-size(20), _selector::binary-size(4), owner_word::binary-size(32),
        salt_word::binary-size(32)>> = Provisioning.deploy_init_code(@owner, 42)

      assert <<0::96, owner_bytes::binary-size(20)>> = owner_word
      assert "0x" <> Base.encode16(owner_bytes, case: :lower) == @owner
      assert <<salt::unsigned-big-256>> = salt_word
      assert salt == 42
    end
  end

  describe "install_session_key_calldata/3" do
    # Reference calldata generated by viem's encodeFunctionData against
    # the installValidation ABI, with the single-signer module, entity
    # 1, flags 7 (userOp|signature|global), empty selectors/hooks, and
    # installData = abi.encode(uint32 entityId, address signer).
    @viem_ref "0x1bbf564c00000000000099de0bf6fa90deb851e2a2df7d83000000010700000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009a96e767bfcce8e80370be00821ed5ba283d4a170000000000000000000000000000000000000000000000000000000000000000"

    test "matches the viem encodeFunctionData reference byte-for-byte" do
      cd = Provisioning.install_session_key_calldata(@owner, 1)
      assert "0x" <> Base.encode16(cd, case: :lower) == @viem_ref
    end

    test "selector is installValidation(bytes25,bytes4[],bytes,bytes[])" do
      cd = Provisioning.install_session_key_calldata(@owner, 1)
      <<selector::binary-size(4), _::binary>> = cd

      expected =
        binary_part(ExKeccak.hash_256("installValidation(bytes25,bytes4[],bytes,bytes[])"), 0, 4)

      assert selector == expected
    end

    test "validationConfig packs module ++ entityId ++ flags(7) in the first word" do
      <<_selector::binary-size(4), config_word::binary-size(32), _rest::binary>> =
        Provisioning.install_session_key_calldata(@owner, 5)

      <<module::binary-size(20), entity::unsigned-big-32, flags::8, _pad::binary>> = config_word

      assert "0x" <> Base.encode16(module, case: :lower) ==
               String.downcase(ModularAccount.single_signer_validation_address())

      assert entity == 5
      assert flags == 7
    end

    test "flags reflect the validation options" do
      <<_::binary-size(4), config_word::binary-size(32), _::binary>> =
        Provisioning.install_session_key_calldata(@owner, 1,
          global: false,
          signature_validation: false
        )

      <<_module::binary-size(20), _entity::unsigned-big-32, flags::8, _::binary>> = config_word
      # only user_op_validation (1)
      assert flags == 1
    end
  end
end
