# `DoubleDown.Double`
[🔗](https://github.com/mccraigmccraig/double_down/blob/main/lib/double_down/double.ex#L1)

Mox-style expect/stub handler declarations with immediate effect.

Each `expect` and `stub` call writes directly to NimbleOwnership —
no builder struct, no `install!` step. Functions return the contract
module atom for Mimic-style piping.

## Basic usage

    DoubleDown.Double.expect(MyContract, :get_thing, fn [id] -> %Thing{id: id} end)
    DoubleDown.Double.stub(MyContract, :list, fn [_] -> [] end)

    # ... run code under test ...

    DoubleDown.Double.verify!()

## Piping

All functions return the contract module, so you can pipe:

    MyContract
    |> DoubleDown.Double.expect(:get_thing, fn [id] -> %Thing{id: id} end)
    |> DoubleDown.Double.stub(:list, fn [_] -> [] end)

## Sequenced expectations

Successive calls to `expect` for the same operation queue handlers
that are consumed in order:

    MyContract
    |> DoubleDown.Double.expect(:get_thing, fn [_] -> {:error, :not_found} end)
    |> DoubleDown.Double.expect(:get_thing, fn [id] -> %Thing{id: id} end)

    # First call returns :not_found, second returns the thing

## Repeated expectations

Use `times: n` when the same function should handle multiple calls:

    DoubleDown.Double.expect(MyContract, :check, fn [_] -> :ok end, times: 3)

## Expects + stubs

When an operation has both expects and a stub, expects are consumed
first; once exhausted, the stub handles all subsequent calls:

    MyContract
    |> DoubleDown.Double.expect(:get, fn [_] -> :first end)
    |> DoubleDown.Double.stub(:get, fn [_] -> :default end)

## Stubs and fakes as fallbacks

A fallback handles any operation without a specific expect or
per-operation stub. Stubs and fakes serve different purposes:

### Function fallback (stub)

A stateless 2-arity `fn operation, args -> result end` — canned
responses, same signature as `set_fn_handler`:

    MyContract
    |> DoubleDown.Double.expect(:get, fn [id] -> %Thing{id: id} end)
    |> DoubleDown.Double.stub(fn
      :list, [_] -> []
      :count, [] -> 0
    end)

### Stateful fake

A 4-arity `fn contract, operation, args, state -> {result, new_state} end`
or 5-arity `fn contract, operation, args, state, all_states -> {result, new_state} end`
with real logic and state. Integrates fakes like `Repo.OpenInMemory`
while allowing expects to override specific calls. 5-arity fakes
receive a read-only snapshot of all contract states for
cross-contract state access:

    # First insert fails, rest go through InMemory
    DoubleDown.Repo
    |> DoubleDown.Double.fake(&Repo.OpenInMemory.dispatch/4, Repo.OpenInMemory.new())
    |> DoubleDown.Double.expect(:insert, fn [changeset] ->
      {:error, Ecto.Changeset.add_error(changeset, :email, "taken")}
    end)

When a 1-arity expect short-circuits (e.g. returning an error), the
fake state is unchanged — correct for error simulation.

Expects can also be stateful — 2-arity and 3-arity responders
receive the fake's state and can update it:

    # 2-arity: access and update the fake's state
    |> DoubleDown.Double.expect(:insert, fn [changeset], state ->
      {result, new_state}
    end)

    # 3-arity: cross-contract state access too
    |> DoubleDown.Double.expect(:insert, fn [changeset], state, all_states ->
      {result, new_state}
    end)

Stateful responders require `fake/3` to be called first (the fake
provides the state). They must return `{result, new_state}`.

### Module fake

A module implementing the contract's `@behaviour`:

    MyContract
    |> DoubleDown.Double.expect(:get, fn [_] -> {:error, :not_found} end)
    |> DoubleDown.Double.fake(MyApp.Impl)

**Mimic-style limitation:** if the module's `:bar` internally calls
`:foo`, and you've stubbed `:foo`, the module won't see your stub —
it calls its own `:foo` directly. For stubs to be visible, the
module must call through the facade.

Dispatch priority: expects > per-operation stubs > fallback/fake > raise.
Function stub, stateful fake, and module fake are mutually
exclusive — setting one replaces the other.

## Passthrough expects

When a fallback/fake is configured, pass `:passthrough` instead of
a function to delegate while still consuming the expect for
`verify!` counting:

    MyContract
    |> DoubleDown.Double.fake(MyApp.Impl)
    |> DoubleDown.Double.expect(:get, :passthrough, times: 2)

## Multi-contract

    DoubleDown.Repo
    |> DoubleDown.Double.fake(&Repo.OpenInMemory.dispatch/4, Repo.OpenInMemory.new())
    |> DoubleDown.Double.expect(:insert, fn [cs] -> {:error, :taken} end)

    QueriesContract
    |> DoubleDown.Double.expect(:get_record, fn [id] -> %Record{id: id} end)

## Relationship to Mox

| Mox | DoubleDown.Double |
|-----|-----------------|
| `expect(Mock, :fn, n, fun)` | `expect(Contract, :fn, fun, times: n)` |
| `stub(Mock, :fn, fun)` | `stub(Contract, :fn, fun)` — per-operation |
| (no equivalent) | `stub(Contract, fn op, args -> ... end)` — function fallback |
| (no equivalent) | `fake(Contract, fn op, args, state -> ... end, init)` — stateful fake (3 or 4-arity) |
| (no equivalent) | `fake(Contract, ImplModule)` — module fake |
| `verify!()` | `verify!()` |
| `verify_on_exit!()` | `verify_on_exit!()` |
| `Mox.defmock(Mock, for: Behaviour)` | Not needed |
| `Application.put_env(...)` | Not needed |

## Relationship to existing APIs

This is a higher-level convenience built on `set_stateful_handler`.
It does not replace `set_fn_handler` or `set_stateful_handler` —
those remain for cases that don't fit the expect/stub pattern.

# `allow`

```elixir
@spec allow(module(), pid(), pid() | (-&gt; pid() | [pid()])) :: :ok | {:error, term()}
```

Allow a child process to use the current process's test doubles.

Delegates to `DoubleDown.Testing.allow/3`. Use this when spawning
Tasks or other processes that need to dispatch through the same
test handlers.

    {:ok, pid} = MyApp.Worker.start_link([])
    DoubleDown.Double.allow(MyContract, pid)

Also accepts a lazy pid function for processes that don't exist
yet at setup time:

    DoubleDown.Double.allow(MyContract, fn -> GenServer.whereis(MyWorker) end)

# `dynamic`

```elixir
@spec dynamic(module()) :: module()
```

Set up a dynamically-faked module with its original implementation
as the fallback.

Requires the module to have been set up with
`DoubleDown.DynamicFacade.setup/1`. Layer expects and stubs on top:

    SomeClient
    |> DoubleDown.Double.dynamic()
    |> DoubleDown.Double.expect(:fetch, fn [_] -> {:error, :timeout} end)

Calls without a matching expect or stub delegate to the original
module's implementation.

Returns the module for piping.

# `expect`

```elixir
@spec expect(module(), atom(), function() | :passthrough, keyword()) :: module()
```

Add an expectation for a contract operation.

The responder function may be:

  * **1-arity** `fn [args] -> result end` — stateless, returns a bare result
  * **2-arity** `fn [args], state -> {result, new_state} end` — reads and
    updates the stateful fake's state. Requires `fake/3` first.
  * **3-arity** `fn [args], state, all_states -> {result, new_state} end` —
    same as 2-arity plus a read-only snapshot of all contract states for
    cross-contract access. Requires `fake/3` first.

Expectations are consumed in order — the first `expect` for an
operation handles the first call, the second handles the second,
and so on.

Instead of a function, pass `:passthrough` to delegate to the
fallback (fn, stateful, or module) while still consuming the
expect for `verify!` counting.

Returns the contract module for piping.

## Options

  * `:times` — enqueue the same function `n` times (default 1).
    Equivalent to calling `expect` `n` times with the same function.

# `fake`

```elixir
@spec fake(module(), module()) :: module()
```

Set a fake implementation as the fallback for a contract.

Fakes have real logic — they maintain state or delegate to a real
implementation module. They handle any operation not covered by an
`expect` or per-operation `stub`.

## FakeHandler module (recommended for stateful fakes)

A module implementing `DoubleDown.Contract.Dispatch.FakeHandler`. The module's
`new/2` builds initial state, and its `dispatch/4` or `dispatch/5`
handles operations:

    # Default state
    DoubleDown.Double.fake(MyContract, Repo.OpenInMemory)

    # With seed data
    DoubleDown.Double.fake(MyContract, Repo.OpenInMemory, [%User{id: 1}])

    # With seed data and options
    DoubleDown.Double.fake(MyContract, Repo.OpenInMemory, [%User{id: 1}],
      fallback_fn: fn :all, [User], state -> Map.values(state[User]) end
    )

## Module fake

A module implementing the contract's `@behaviour` (but not FakeHandler).
All unhandled operations delegate via `apply(module, operation, args)`:

    DoubleDown.Double.fake(MyContract, MyApp.Impl)

The module is validated immediately — all contract operations must
be exported.

**Mimic-style limitation:** if the module's `:bar` internally calls
`:foo`, and you've stubbed `:foo`, the module won't see your stub —
it calls its own `:foo` directly. For stubs to be visible, the
module must call through the facade.

## Stateful fake function

A 4-arity `fn contract, operation, args, state -> {result, new_state} end`
or 5-arity `fn contract, operation, args, state, all_states -> {result, new_state} end`
with initial state:

    DoubleDown.Double.fake(MyContract, &handler/4, initial_state)

The fake's state is threaded through calls automatically. When an
expect short-circuits (e.g. returning an error), the fake state is
unchanged — correct for error simulation.

Dispatch priority: expects > per-operation stubs > fake > raise.
Function fallback (`stub/2`), module fake, and stateful fake are
mutually exclusive — setting one replaces the other.

Returns the contract module for piping.

# `fake`

```elixir
@spec fake(module(), function() | module(), term()) :: module()
```

# `fake`

```elixir
@spec fake(module(), module(), term(), keyword()) :: module()
```

# `passthrough`

```elixir
@spec passthrough() :: DoubleDown.Contract.Dispatch.Passthrough.t()
```

Return a passthrough sentinel for use in expect responders.

When returned from an expect responder, delegates the call to the
fallback/fake as if the expect had been registered with `:passthrough`.
The expect is still consumed for `verify!` counting.

This enables conditional passthrough — the responder can inspect
the state and decide whether to handle the call or delegate:

    DoubleDown.Repo
    |> Double.fake(&Repo.OpenInMemory.dispatch/4, Repo.OpenInMemory.new())
    |> Double.expect(:insert, fn [changeset], state ->
      if duplicate?(state, changeset) do
        {{:error, add_error(changeset, :email, "taken")}, state}
      else
        Double.passthrough()
      end
    end)

# `stub`

```elixir
@spec stub(module(), function() | module()) :: module()
```

Add a stub for a contract operation or a stateless function fallback.

## Per-operation stub

The function receives the argument list and returns the result.
Stubs handle any number of calls and are used after all expectations
for an operation are consumed. Setting a stub twice for the same
operation replaces the previous one.

The function may be:

  * **1-arity** `fn [args] -> result end` — stateless
  * **2-arity** `fn [args], state -> {result, new_state} end` — stateful
    (requires `fake/3` first)
  * **3-arity** `fn [args], state, all_states -> {result, new_state} end` —
    cross-contract state access (requires `fake/3` first)

Any arity may return `Double.passthrough()` to delegate to the
fallback/fake for that specific call.

    DoubleDown.Double.stub(MyContract, :list, fn [_] -> [] end)

## Function fallback (2-arity function)

When the function is 2-arity `fn operation, args -> result end`,
it acts as a fallback for any operation on the contract that has
no per-operation expect or stub. This is the same signature as
`set_fn_handler`, so existing handler functions can be reused:

    DoubleDown.Double.stub(MyContract, fn
      :list, [_] -> []
      :count, [] -> 0
    end)

## StubHandler module

A module implementing `DoubleDown.Contract.Dispatch.StubHandler`. The module's
`new/2` builds a dispatch function from an optional fallback:

    # Writes only — reads will raise
    DoubleDown.Double.stub(MyContract, DoubleDown.Repo.Stub)

    # With fallback for reads
    DoubleDown.Double.stub(MyContract, DoubleDown.Repo.Stub,
      fn :all, [User] -> [] end)

For stateful fakes and module delegation, see `fake/2` and `fake/3`.

Dispatch priority: expects > per-operation stubs > fallback/fake > raise.
Function fallback, StubHandler, stateful fake, and module fake are
mutually exclusive — setting one replaces the other.

Returns the contract module for piping.

# `stub`

```elixir
@spec stub(module(), atom(), function() | nil) :: module()
```

# `stub`

```elixir
@spec stub(module(), module(), (atom(), [term()] -&gt; term()) | nil, keyword()) ::
  module()
```

# `verify!`

```elixir
@spec verify!() :: :ok
```

Verify that all expectations have been consumed.

Reads the current handler state for each contract and checks that
all expect queues are empty. Stubs are not checked — they are
allowed to be called zero or more times.

Raises with a descriptive message if any expectations remain
unconsumed.

Returns `:ok` if all expectations are satisfied.

# `verify!`

```elixir
@spec verify!(pid()) :: :ok
```

Verify expectations for a specific process.

Same as `verify!/0` but checks the expectations owned by `pid`
instead of the calling process. Used internally by `verify_on_exit!/0`.

# `verify_on_exit!`

```elixir
@spec verify_on_exit!(map()) :: :ok
```

Register an `on_exit` callback that verifies expectations after
each test.

Call this in a `setup` block so that tests which forget to call
`verify!/0` explicitly still fail on unconsumed expectations:

    setup :verify_on_exit!

Or equivalently:

    setup do
      DoubleDown.Double.verify_on_exit!()
    end

The verification runs in the on_exit callback (a separate process),
using the test pid captured at setup time.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
