<?php
/**
 * Migration result value object.
 *
 * Captures the outcome of a single {@see MigrationRunner::run()}
 * invocation: which schema version the runner started from, which
 * version it ended at, and the ordered list of migrations that were
 * applied during the run. Returned to the caller so activation flows
 * and diagnostics can report exactly what happened — in particular,
 * whether the run was a no-op (no pending migrations) or advanced the
 * schema by one or more steps.
 *
 * Instances are immutable. They are constructed by the runner after the
 * pipeline completes; callers do not assemble them directly.
 *
 * Typical consumers:
 *
 *   - Activation code logs `"applied N migrations, now at vX"` using
 *     {@see self::appliedCount()} and {@see self::endVersion()}.
 *
 *   - Diagnostics surfaces expose {@see self::appliedDescriptions()}
 *     so operators can see the human-readable story of what changed.
 *
 *   - Tests assert on {@see self::startVersion()} /
 *     {@see self::endVersion()} to verify fresh-install vs. existing-
 *     environment migration paths.
 *
 * @package Affilun\Infrastructure\Persistence
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence;

/**
 * Immutable outcome record for a {@see MigrationRunner::run()} call.
 *
 * See the file-level docblock for how callers are expected to consume
 * the recorded fields.
 */
final class MigrationResult {

	/**
	 * Schema version installed before the run began.
	 */
	private SchemaVersion $startVersion;

	/**
	 * Schema version installed after the run completed.
	 *
	 * Equal to {@see self::$startVersion} when no migrations were
	 * pending; otherwise equal to the target version of the last
	 * successfully-applied migration in {@see self::$applied}.
	 */
	private SchemaVersion $endVersion;

	/**
	 * Ordered list of migrations applied during the run.
	 *
	 * Entries are listed in the order they were executed, which matches
	 * ascending target version because the runner sorts its pipeline
	 * before dispatching.
	 *
	 * @var list<Migration>
	 */
	private array $applied;

	/**
	 * Construct the result.
	 *
	 * @param SchemaVersion   $startVersion Schema version before the run.
	 * @param SchemaVersion   $endVersion   Schema version after the run.
	 * @param list<Migration> $applied      Migrations executed, in order.
	 */
	public function __construct(
		SchemaVersion $startVersion,
		SchemaVersion $endVersion,
		array $applied
	) {
		$this->startVersion = $startVersion;
		$this->endVersion   = $endVersion;
		$this->applied      = array_values( $applied );
	}

	/**
	 * Schema version installed before the run began.
	 */
	public function startVersion(): SchemaVersion {
		return $this->startVersion;
	}

	/**
	 * Schema version installed after the run completed.
	 *
	 * Equal to {@see self::startVersion()} when no migrations were
	 * pending; otherwise equal to the target version of the last
	 * successfully-applied migration.
	 */
	public function endVersion(): SchemaVersion {
		return $this->endVersion;
	}

	/**
	 * Ordered list of migrations applied during the run.
	 *
	 * Entries appear in execution order (ascending target version).
	 *
	 * @return list<Migration>
	 */
	public function applied(): array {
		return $this->applied;
	}

	/**
	 * Number of migrations applied during the run.
	 *
	 * Convenience for callers that only need the count (activation
	 * logging, `"applied N migrations"` style messages).
	 */
	public function appliedCount(): int {
		return count( $this->applied );
	}

	/**
	 * Report whether the run applied zero migrations.
	 *
	 * True when the schema was already at (or beyond) the highest
	 * registered target version before the run began.
	 */
	public function isNoop(): bool {
		return $this->applied === array();
	}

	/**
	 * Report whether the run advanced the schema version.
	 *
	 * Logical inverse of {@see self::isNoop()}; exposed as an explicit
	 * method so call sites read naturally (`if ( $result->didAdvance() )`).
	 */
	public function didAdvance(): bool {
		return $this->applied !== array();
	}

	/**
	 * Human-readable descriptions of every applied migration.
	 *
	 * Returns each migration's {@see Migration::description()} in
	 * execution order, suitable for logging or diagnostics surfaces.
	 *
	 * @return list<string>
	 */
	public function appliedDescriptions(): array {
		return array_map(
			static fn ( Migration $migration ): string => $migration->description(),
			$this->applied
		);
	}

	/**
	 * Target versions of every applied migration, in execution order.
	 *
	 * Returned as bare integers (rather than {@see SchemaVersion}
	 * instances) because the primary consumers — log lines, diagnostics
	 * JSON, test assertions — all want the numeric value directly.
	 *
	 * @return list<int>
	 */
	public function appliedVersions(): array {
		return array_map(
			static fn ( Migration $migration ): int => $migration->targetVersion()->toInt(),
			$this->applied
		);
	}
}
