<?php
/**
 * Grid CSS classes value object.
 *
 * Immutable value object that carries the §8.3 `classes` slot a
 * §8.3 ProductGridViewModel exposes to its template. The type
 * collects the two BEM-class axes the §8.3 canonical structure
 * enumerates — `root` and `item` — into a single validated value
 * so renderers and templates can stamp `class="..."` attributes
 * verbatim without re-deriving the BEM token list from a sibling
 * EmbedConfig at render time.
 *
 * This VO is deliberately separate from the
 * {@see \CommerceLinkBuilder\Application\Render\ViewModel\Parts\CssClasses}
 * sibling used by §8.2 ProductCardViewModel: the card-level
 * structure carries six slot-specific BEM classes (root / image /
 * body / title / price / buttons), while the grid-level structure
 * only carries a root class for the grid container and an item
 * class for each grid cell. Reusing the card VO here would force
 * grid call sites to spell unrelated card-internal classes; a
 * dedicated grid-shape VO keeps each layer's `classes` slot
 * minimal and self-explanatory.
 *
 * Field semantics match `specifications/rendering-and-template-spec.md`
 * §8.3 exactly (the `classes` sub-object in the canonical
 * ProductGridViewModel structure):
 *
 *   - `root` is the §8.3 outermost-grid-element class string
 *     ("clb-grid clb-grid-default clb-columns-3"). Held verbatim,
 *     non-empty after trimming.
 *
 *   - `item` is the §8.3 grid-cell class string
 *     ("clb-grid-item"). Held verbatim, non-empty after trimming.
 *
 * Instances are immutable. There are no `with*()` mutation helpers
 * because a grid-classes view is composed once per render at the
 * ViewModel-factory boundary.
 *
 * @package CommerceLinkBuilder\Application\Render\ViewModel\Parts
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Application\Render\ViewModel\Parts;

use InvalidArgumentException;

/**
 * Immutable §8.3 grid-classes view used by ProductGridViewModel.
 *
 * See the file-level docblock for the mapping to
 * `rendering-and-template-spec.md` §8.3 and the per-field
 * validation rules.
 */
final class GridClasses {

	/**
	 * Pre-tokenized grid-root BEM class string.
	 */
	private string $root;

	/**
	 * Pre-tokenized grid-item BEM class string.
	 */
	private string $item;

	/**
	 * Construct a grid-classes view with explicit values for
	 * every axis.
	 *
	 * Validation summary:
	 *
	 *   - `root` is trimmed and rejected when empty after trimming.
	 *   - `item` is trimmed and rejected when empty after trimming.
	 *
	 * @param string $root §8.3 grid-root class string.
	 * @param string $item §8.3 grid-item class string.
	 *
	 * @throws InvalidArgumentException When any field violates the
	 *                                  per-field rules summarized
	 *                                  above.
	 */
	public function __construct( string $root, string $item ) {
		$this->root = self::normalizeNonEmpty( 'root', $root );
		$this->item = self::normalizeNonEmpty( 'item', $item );
	}

	/**
	 * §8.3 grid-root class string.
	 */
	public function root(): string {
		return $this->root;
	}

	/**
	 * §8.3 grid-item class string.
	 */
	public function item(): string {
		return $this->item;
	}

	/**
	 * Report whether this view equals the supplied one.
	 *
	 * Equality is structural: every field must match with strict
	 * comparison.
	 *
	 * @param self $other Other view to compare against.
	 */
	public function equals( self $other ): bool {
		return $this->root === $other->root
			&& $this->item === $other->item;
	}

	/**
	 * Project this view to its §8.3 canonical array shape.
	 *
	 * @return array{root: string, item: string}
	 */
	public function toArray(): array {
		return array(
			'root' => $this->root,
			'item' => $this->item,
		);
	}

	/**
	 * Validate and normalize a non-empty class-string field at
	 * construction.
	 *
	 * @param string $field Field name for the error message.
	 * @param string $value Raw value as supplied by the caller.
	 *
	 * @throws InvalidArgumentException When `$value` is empty
	 *                                  after trimming.
	 */
	private static function normalizeNonEmpty( string $field, string $value ): string {
		$trimmed = trim( $value );
		if ( $trimmed === '' ) {
			$message = sprintf(
				'GridClasses %s must be a non-empty string.',
				$field
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		return $trimmed;
	}
}
