<?php
/**
 * CSS classes value object.
 *
 * Immutable value object that carries the §8.2 `classes` slot
 * pre-tokenized BEM strings a §8.2 ProductCardViewModel exposes to
 * its template. The type collects the six BEM-class axes the §8.2
 * canonical structure enumerates — `root`, `image`, `body`,
 * `title`, `price`, `buttons` — into a single validated value so
 * renderers and templates can stamp `class="..."` attributes
 * verbatim without re-deriving the BEM token list from a sibling
 * RenderContext or template-key map at render time.
 *
 * Field semantics match `specifications/rendering-and-template-spec.md`
 * §8.2 exactly (the `classes` sub-object in the canonical
 * ProductCardViewModel structure):
 *
 *   - `root` is the §8.2 outermost-element class string the
 *     renderer stamps onto the card root element. The canonical
 *     shape is `affilun-card affilun-card-{template} affilun-provider-{provider}`
 *     ("affilun-card affilun-card-default affilun-provider-amazon"); the
 *     {@see self::defaultsForTemplate()} factory produces it from
 *     the template-key and provider-key inputs.
 *
 *   - `image` is the §8.2 image-region BEM class string
 *     ("affilun-card__image"). Held verbatim, non-empty.
 *
 *   - `body` is the §8.2 body-region BEM class string
 *     ("affilun-card__body"). Held verbatim, non-empty.
 *
 *   - `title` is the §8.2 title-element BEM class string
 *     ("affilun-card__title"). Held verbatim, non-empty.
 *
 *   - `price` is the §8.2 price-region BEM class string
 *     ("affilun-card__price"). Held verbatim, non-empty.
 *
 *   - `buttons` is the §8.2 buttons-region BEM class string
 *     ("affilun-card__buttons"). Held verbatim, non-empty.
 *
 * The canonical "default for template / provider" branch is
 * exposed as a static factory {@see self::defaultsForTemplate()}
 * so call sites do not have to re-spell the standard BEM token
 * list for every render. Both inputs are validated against the
 * `^[a-z0-9-]+$` grammar so a template-key or provider-key typo
 * never reaches the rendered class attribute.
 *
 * Instances are immutable. There are no `with*()` mutation helpers
 * because a CSS-classes view is composed once per render at the
 * ViewModel-factory boundary.
 *
 * @package Affilun\Application\Render\ViewModel\Parts
 */

declare(strict_types=1);

namespace Affilun\Application\Render\ViewModel\Parts;

use InvalidArgumentException;

/**
 * Immutable CSS-classes view used by §8.2 ProductCardViewModel.
 *
 * See the file-level docblock for the mapping to
 * `rendering-and-template-spec.md` §8.2 and the per-field
 * validation rules.
 */
final class CssClasses {

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

	/**
	 * Pre-tokenized image-region BEM class string.
	 */
	private string $image;

	/**
	 * Pre-tokenized body-region BEM class string.
	 */
	private string $body;

	/**
	 * Pre-tokenized title-element BEM class string.
	 */
	private string $title;

	/**
	 * Pre-tokenized price-region BEM class string.
	 */
	private string $price;

	/**
	 * Pre-tokenized buttons-region BEM class string.
	 */
	private string $buttons;

	/**
	 * Construct a CSS-classes view with explicit values for every
	 * field.
	 *
	 * Validation summary:
	 *
	 *   - Every field is trimmed and rejected when empty after
	 *     trimming. The internal token grammar is not enforced at
	 *     this layer because callers may legitimately stamp
	 *     site-specific BEM extensions onto each axis; the
	 *     {@see self::defaultsForTemplate()} factory produces the
	 *     canonical baseline.
	 *
	 * @param string $root    Pre-tokenized root-element class string.
	 * @param string $image   Pre-tokenized image-region class string.
	 * @param string $body    Pre-tokenized body-region class string.
	 * @param string $title   Pre-tokenized title-element class string.
	 * @param string $price   Pre-tokenized price-region class string.
	 * @param string $buttons Pre-tokenized buttons-region class string.
	 *
	 * @throws InvalidArgumentException When any field is empty
	 *                                  after trimming.
	 */
	public function __construct(
		string $root,
		string $image,
		string $body,
		string $title,
		string $price,
		string $buttons
	) {
		$this->root    = self::normalizeRequiredString( 'root', $root );
		$this->image   = self::normalizeRequiredString( 'image', $image );
		$this->body    = self::normalizeRequiredString( 'body', $body );
		$this->title   = self::normalizeRequiredString( 'title', $title );
		$this->price   = self::normalizeRequiredString( 'price', $price );
		$this->buttons = self::normalizeRequiredString( 'buttons', $buttons );
	}

	/**
	 * Construct a CSS-classes view with the canonical §8.2
	 * defaults derived from a template key and a provider key.
	 *
	 * Produces the §8.2 example structure verbatim:
	 *
	 *   - `root`    = `affilun-card affilun-card-{template} affilun-provider-{provider}`
	 *   - `image`   = `affilun-card__image`
	 *   - `body`    = `affilun-card__body`
	 *   - `title`   = `affilun-card__title`
	 *   - `price`   = `affilun-card__price`
	 *   - `buttons` = `affilun-card__buttons`
	 *
	 * Both inputs MUST match `^[a-z0-9-]+$` so a template-key or
	 * provider-key typo never reaches the rendered class
	 * attribute.
	 *
	 * @param string $template §6.4 template key (lowercase ASCII / digits / hyphens).
	 * @param string $provider Lowercase provider key (ASCII / digits / hyphens).
	 *
	 * @throws InvalidArgumentException When either input does not
	 *                                  match the `^[a-z0-9-]+$`
	 *                                  grammar.
	 */
	public static function defaultsForTemplate( string $template, string $provider ): self {
		$normalizedTemplate = self::normalizeToken( 'template', $template );
		$normalizedProvider = self::normalizeToken( 'provider', $provider );

		return new self(
			sprintf(
				'affilun-card affilun-card-%s affilun-provider-%s',
				$normalizedTemplate,
				$normalizedProvider
			),
			'affilun-card__image',
			'affilun-card__body',
			'affilun-card__title',
			'affilun-card__price',
			'affilun-card__buttons'
		);
	}

	/**
	 * Pre-tokenized root-element BEM class string.
	 */
	public function root(): string {
		return $this->root;
	}

	/**
	 * Pre-tokenized image-region BEM class string.
	 */
	public function image(): string {
		return $this->image;
	}

	/**
	 * Pre-tokenized body-region BEM class string.
	 */
	public function body(): string {
		return $this->body;
	}

	/**
	 * Pre-tokenized title-element BEM class string.
	 */
	public function title(): string {
		return $this->title;
	}

	/**
	 * Pre-tokenized price-region BEM class string.
	 */
	public function price(): string {
		return $this->price;
	}

	/**
	 * Pre-tokenized buttons-region BEM class string.
	 */
	public function buttons(): string {
		return $this->buttons;
	}

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

	/**
	 * Validate and normalize a required 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 normalizeRequiredString( string $field, string $value ): string {
		$trimmed = trim( $value );
		if ( $trimmed === '' ) {
			$message = sprintf(
				'CssClasses %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;
	}

	/**
	 * Validate a `^[a-z0-9-]+$` token at construction.
	 *
	 * Used by {@see self::defaultsForTemplate()} for the template-
	 * key and provider-key inputs so a typo (uppercase character,
	 * embedded space, underscore) never reaches the rendered
	 * class attribute.
	 *
	 * @param string $field Field name for the error message.
	 * @param string $value Raw token as supplied by the caller.
	 *
	 * @throws InvalidArgumentException When `$value` does not
	 *                                  match `^[a-z0-9-]+$`.
	 */
	private static function normalizeToken( string $field, string $value ): string {
		if ( preg_match( '/\A[a-z0-9-]+\z/', $value ) !== 1 ) {
			$message = sprintf(
				'CssClasses %s must match /^[a-z0-9-]+$/; got "%s".',
				$field,
				$value
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		return $value;
	}
}
