<?php

declare(strict_types=1);

namespace Drupal\Tests\nodehive_path_alias\Unit;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\nodehive_path_alias\Plugin\Validation\Constraint\NodehiveSpaceUniquePathAliasConstraint;
use Drupal\nodehive_path_alias\Plugin\Validation\Constraint\NodehiveSpaceUniquePathAliasConstraintValidator;
use Drupal\path_alias\PathAliasInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;

/**
 * Unit tests for NodehiveSpaceUniquePathAliasConstraintValidator.
 */
#[CoversClass(NodehiveSpaceUniquePathAliasConstraintValidator::class)]
#[Group('nodehive_path_alias')]
final class NodehiveSpaceUniquePathAliasConstraintValidatorTest extends TestCase {

  /**
   * The constraint under test.
   *
   * @var \Drupal\nodehive_path_alias\Plugin\Validation\Constraint\NodehiveSpaceUniquePathAliasConstraint
   */
  private NodehiveSpaceUniquePathAliasConstraint $constraint;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->constraint = new NodehiveSpaceUniquePathAliasConstraint();
  }

  /**
   * Tests that no violation is added when the alias is unique.
   */
  public function testNoViolationWhenAliasIsUnique(): void {
    $query = $this->buildQuery([]);
    $storage = $this->buildStorage($query);

    $entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
    $entityTypeManager->method('getStorage')->willReturn($storage);

    $context = $this->createMock(ExecutionContextInterface::class);
    $context->expects(self::never())->method('buildViolation');

    $validator = new NodehiveSpaceUniquePathAliasConstraintValidator($entityTypeManager);
    $validator->initialize($context);

    $alias = $this->buildAlias('/about', '/node/1', 'cs', 1);
    $validator->validate($alias, $this->constraint);
  }

  /**
   * Tests that a violation is added for a duplicate alias in the same space.
   */
  public function testViolationWhenDuplicateAliasInSameSpace(): void {
    $existingAlias = $this->buildAlias('/about', '/node/2', 'cs', 1, FALSE, 99);

    $query = $this->buildQuery([99]);
    $storage = $this->buildStorage($query, $existingAlias);

    $entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
    $entityTypeManager->method('getStorage')->willReturn($storage);

    $violationBuilder = $this->createMock(ConstraintViolationBuilderInterface::class);
    $violationBuilder->method('addViolation');

    $context = $this->createMock(ExecutionContextInterface::class);
    $context->expects(self::once())
      ->method('buildViolation')
      ->with($this->constraint->message, ['%alias' => '/about'])
      ->willReturn($violationBuilder);

    $validator = new NodehiveSpaceUniquePathAliasConstraintValidator($entityTypeManager);
    $validator->initialize($context);

    $alias = $this->buildAlias('/about', '/node/1', 'cs', 1);
    $validator->validate($alias, $this->constraint);
  }

  /**
   * Tests that a capitalization violation message is used when aliases differ.
   */
  public function testDifferentCapitalizationViolation(): void {
    $existingAlias = $this->buildAlias('/About', '/node/2', 'cs', 1, FALSE, 99);

    $query = $this->buildQuery([99]);
    $storage = $this->buildStorage($query, $existingAlias);

    $entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
    $entityTypeManager->method('getStorage')->willReturn($storage);

    $violationBuilder = $this->createMock(ConstraintViolationBuilderInterface::class);
    $violationBuilder->method('addViolation');

    $context = $this->createMock(ExecutionContextInterface::class);
    $context->expects(self::once())
      ->method('buildViolation')
      ->with($this->constraint->differentCapitalizationMessage, [
        '%alias' => '/about',
        '%stored_alias' => '/About',
      ])
      ->willReturn($violationBuilder);

    $validator = new NodehiveSpaceUniquePathAliasConstraintValidator($entityTypeManager);
    $validator->initialize($context);

    $alias = $this->buildAlias('/about', '/node/1', 'cs', 1);
    $validator->validate($alias, $this->constraint);
  }

  /**
   * Tests that no violation occurs for the same alias in a different space.
   */
  public function testNoViolationForSameAliasInDifferentSpace(): void {
    // The query is scoped to the entity's space_id, so the DB returns empty.
    // Alias /homepage exists in space 1; we are creating it in space 2.
    $query = $this->buildQuery([]);
    $storage = $this->buildStorage($query);

    $entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
    $entityTypeManager->method('getStorage')->willReturn($storage);

    $context = $this->createMock(ExecutionContextInterface::class);
    $context->expects(self::never())->method('buildViolation');

    $validator = new NodehiveSpaceUniquePathAliasConstraintValidator($entityTypeManager);
    $validator->initialize($context);

    $alias = $this->buildAlias('/homepage', '/node/3', 'cs', 2);
    $validator->validate($alias, $this->constraint);
  }

  /**
   * Builds a mock PathAliasInterface with the given field values.
   */
  private function buildAlias(string $alias, string $path, string $langcode, ?int $spaceId, bool $isNew = TRUE, ?int $id = NULL): PathAliasInterface {
    $language = $this->createMock(LanguageInterface::class);
    $language->method('getId')->willReturn($langcode);

    $spaceField = new class($spaceId) {

      public function __construct(public readonly ?int $target_id) {}

    };

    $entity = $this->createMock(PathAliasInterface::class);
    $entity->method('getAlias')->willReturn($alias);
    $entity->method('getPath')->willReturn($path);
    $entity->method('language')->willReturn($language);
    $entity->method('get')->with('nodehive_space')->willReturn($spaceField);
    $entity->method('isNew')->willReturn($isNew);
    $entity->method('id')->willReturn($id);

    return $entity;
  }

  /**
   * Builds a mock entity query that returns the given IDs.
   */
  private function buildQuery(array $foundIds): QueryInterface {
    $query = $this->createMock(QueryInterface::class);
    $query->method('accessCheck')->willReturnSelf();
    $query->method('condition')->willReturnSelf();
    $query->method('notExists')->willReturnSelf();
    $query->method('range')->willReturnSelf();
    $query->method('execute')->willReturn($foundIds);

    return $query;
  }

  /**
   * Builds a mock entity storage that returns the given query and alias.
   */
  private function buildStorage(QueryInterface $query, ?PathAliasInterface $existingAlias = NULL): EntityStorageInterface {
    $storage = $this->createMock(EntityStorageInterface::class);
    $storage->method('getQuery')->willReturn($query);
    if ($existingAlias !== NULL) {
      $storage->method('load')->willReturn($existingAlias);
    }

    return $storage;
  }

}
