<?php
/**
 * In-memory option store for tests.
 *
 * Provides an {@see OptionStore} implementation that keeps every stored
 * value in a plain PHP array on the instance, with no dependency on
 * WordPress globals or the options table. This is the canonical test
 * double used by pure-unit tests under `tests/Unit/Infrastructure/
 * Persistence/`; integration tests that need WordPress semantics should
 * exercise {@see WpOptionStore} against a real database instead.
 *
 * Semantic parity with {@see WpOptionStore}:
 *
 *   - {@see self::get()} returns the caller-supplied default verbatim
 *     when the key is unset, preserving its type (including `null`,
 *     `false`, and `0`).
 *
 *   - {@see self::update()} is an upsert. It returns true when the
 *     stored value changed as a result of the call and false when the
 *     write was a no-op (identical value already stored), matching
 *     WordPress's `update_option()` return convention.
 *
 *   - {@see self::delete()} returns true when a value existed prior to
 *     deletion and false when no value was stored under the key.
 *
 *   - {@see self::has()} distinguishes "unset" from "stored as a falsy
 *     value" by checking key presence directly, so callers can reliably
 *     probe for stored `null` / `false` / `0` entries.
 *
 * The store exposes {@see self::all()} as a convenience for assertions;
 * production code MUST NOT rely on it because it has no counterpart on
 * the {@see OptionStore} contract.
 *
 * @package CommerceLinkBuilder\Infrastructure\Persistence
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Infrastructure\Persistence;

/**
 * Array-backed {@see OptionStore} implementation for unit tests.
 *
 * See the file-level docblock for the semantic parity guarantees this
 * double maintains with {@see WpOptionStore}.
 */
final class InMemoryOptionStore implements OptionStore {

	/**
	 * Stored key/value pairs.
	 *
	 * Keyed by the option name passed to {@see self::update()}. Values
	 * are stored verbatim so equality checks in {@see self::update()}
	 * can use strict comparison to detect no-op writes.
	 *
	 * @var array<string, mixed>
	 */
	private array $values = array();

	/**
	 * Construct the store, optionally seeded with an initial set of
	 * key/value pairs.
	 *
	 * Seeding is primarily useful for tests that need to exercise the
	 * "existing environment" migration path, where the schema version
	 * option is already populated before the code under test runs.
	 *
	 * @param array<string, mixed> $seed Initial key/value pairs.
	 */
	public function __construct( array $seed = array() ) {
		foreach ( $seed as $key => $value ) {
			$this->values[ (string) $key ] = $value;
		}
	}

	/**
	 * Read the value stored under the given key.
	 *
	 * Returns the caller-supplied fallback verbatim when the key is unset,
	 * preserving its type (including `null`, `false`, and `0`).
	 *
	 * The parameter is named `$fallback` rather than `$default` to avoid
	 * the PHP reserved keyword warning raised by PHPCS's
	 * Universal.NamingConventions.NoReservedKeywordParameterNames sniff;
	 * the semantics are identical to WordPress's `get_option( $key, $default )`.
	 *
	 * @param string $key      Option key.
	 * @param mixed  $fallback Value returned when the key is unset.
	 *
	 * @return mixed The stored value, or `$fallback` when unset.
	 */
	public function get( string $key, $fallback = null ) {
		if ( ! array_key_exists( $key, $this->values ) ) {
			return $fallback;
		}

		return $this->values[ $key ];
	}

	/**
	 * Write the value under the given key (upsert).
	 *
	 * Returns true when the stored value changed as a result of the call
	 * and false when the write was a no-op (identical value already
	 * stored), matching WordPress's `update_option()` return convention.
	 *
	 * @param string $key   Option key.
	 * @param mixed  $value Value to store.
	 *
	 * @return bool True when the stored value changed, false otherwise.
	 */
	public function update( string $key, $value ): bool {
		if ( array_key_exists( $key, $this->values ) && $this->values[ $key ] === $value ) {
			return false;
		}

		$this->values[ $key ] = $value;
		return true;
	}

	/**
	 * Delete the value stored under the given key.
	 *
	 * @param string $key Option key.
	 *
	 * @return bool True when a value existed and was removed, false when
	 *              no value was stored under the key.
	 */
	public function delete( string $key ): bool {
		if ( ! array_key_exists( $key, $this->values ) ) {
			return false;
		}

		unset( $this->values[ $key ] );
		return true;
	}

	/**
	 * Report whether a value is stored under the given key.
	 *
	 * Distinguishes "unset" from "stored as a falsy value" by checking
	 * key presence directly, so callers can reliably probe for stored
	 * `null` / `false` / `0` entries.
	 *
	 * @param string $key Option key.
	 */
	public function has( string $key ): bool {
		return array_key_exists( $key, $this->values );
	}

	/**
	 * Return every stored key/value pair.
	 *
	 * Exposed purely as a convenience for test assertions. Production
	 * code MUST NOT rely on this method because it has no counterpart
	 * on the {@see OptionStore} contract.
	 *
	 * @return array<string, mixed>
	 */
	public function all(): array {
		return $this->values;
	}
}
