<?php

declare(strict_types=1);

namespace Drupal\Tests\nodehive_path_alias\Unit;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\StatementInterface;
use Drupal\decoupled_router\PathTranslatorEvent;
use Drupal\nodehive_path_alias\EventSubscriber\ApiTokenPathTranslatorSubscriber;
use Drupal\nodehive_path_alias\PathAliasSpaceLookupInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;

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

  /**
   * Tests that the subscriber skips when the space is already resolved.
   */
  public function testSkipsWhenAlreadyResolved(): void {
    $db = $this->createMock(Connection::class);
    $db->expects(self::never())->method('select');

    $lookup = $this->createMock(PathAliasSpaceLookupInterface::class);
    $lookup->expects(self::never())->method('resolve');

    $event = $this->buildEvent('/about', 'Bearer abc123', TRUE);
    $event->expects(self::never())->method('setPath');

    $subscriber = new ApiTokenPathTranslatorSubscriber($db, $lookup, $this->createMock(LoggerInterface::class));
    $subscriber->onPathTranslation($event);
  }

  /**
   * Tests that the subscriber skips when no Authorization header is present.
   */
  public function testSkipsWhenNoAuthorizationHeader(): void {
    $db = $this->createMock(Connection::class);
    $db->expects(self::never())->method('select');

    $lookup = $this->createMock(PathAliasSpaceLookupInterface::class);
    $lookup->expects(self::never())->method('resolve');

    $event = $this->buildEvent('/about', NULL);
    $event->expects(self::never())->method('setPath');

    $subscriber = new ApiTokenPathTranslatorSubscriber($db, $lookup, $this->createMock(LoggerInterface::class));
    $subscriber->onPathTranslation($event);
  }

  /**
   * Tests that the subscriber skips when the token is not in Bearer format.
   */
  public function testSkipsWhenTokenNotBearerFormat(): void {
    $db = $this->createMock(Connection::class);
    $db->expects(self::never())->method('select');

    $lookup = $this->createMock(PathAliasSpaceLookupInterface::class);

    $event = $this->buildEvent('/about', 'Basic dXNlcjpwYXNz');
    $event->expects(self::never())->method('setPath');

    $subscriber = new ApiTokenPathTranslatorSubscriber($db, $lookup, $this->createMock(LoggerInterface::class));
    $subscriber->onPathTranslation($event);
  }

  /**
   * Tests that the subscriber skips when the token does not match any space.
   */
  public function testSkipsWhenTokenNotMatchingAnySpace(): void {
    // Token not found in the database.
    $db = $this->buildDb(NULL);

    $lookup = $this->createMock(PathAliasSpaceLookupInterface::class);
    $lookup->expects(self::never())->method('resolve');

    $event = $this->buildEvent('/about', 'Bearer unknowntoken');
    $event->expects(self::never())->method('setPath');

    $subscriber = new ApiTokenPathTranslatorSubscriber($db, $lookup, $this->createMock(LoggerInterface::class));
    $subscriber->onPathTranslation($event);
  }

  /**
   * Tests that the subscriber skips when the lookup returns NULL.
   */
  public function testSkipsWhenLookupReturnsNull(): void {
    // Token resolves to space 1.
    $db = $this->buildDb(1);

    $lookup = $this->createMock(PathAliasSpaceLookupInterface::class);
    $lookup->method('resolve')->willReturn(NULL);

    $event = $this->buildEvent('/about', 'Bearer validtoken');
    $event->expects(self::never())->method('setPath');

    $subscriber = new ApiTokenPathTranslatorSubscriber($db, $lookup, $this->createMock(LoggerInterface::class));
    $subscriber->onPathTranslation($event);
  }

  /**
   * Tests that setPath is called when the token and alias both resolve.
   */
  public function testSetsPathWhenTokenAndAliasResolve(): void {
    // Token resolves to space 3.
    $db = $this->buildDb(3);

    $lookup = $this->createMock(PathAliasSpaceLookupInterface::class);
    $lookup->method('resolve')->with('/homepage', 3, NULL)->willReturn('/node/99');

    $event = $this->buildEvent('/homepage', 'Bearer ' . str_repeat('0', 63) . '3');
    $event->expects(self::once())->method('setPath')->with('/node/99');

    $subscriber = new ApiTokenPathTranslatorSubscriber($db, $lookup, $this->createMock(LoggerInterface::class));
    $subscriber->onPathTranslation($event);
  }

  /**
   * Tests that Bearer token matching is case-insensitive.
   */
  public function testBearerTokenIsCaseInsensitive(): void {
    $db = $this->buildDb(1);

    $lookup = $this->createMock(PathAliasSpaceLookupInterface::class);
    $lookup->method('resolve')->willReturn('/node/1');

    $event = $this->buildEvent('/about', 'BEARER mytoken');
    $event->expects(self::once())->method('setPath');

    $subscriber = new ApiTokenPathTranslatorSubscriber($db, $lookup, $this->createMock(LoggerInterface::class));
    $subscriber->onPathTranslation($event);
  }

  /**
   * Tests that getSubscribedEvents returns the correct priority.
   */
  public function testGetSubscribedEventsReturnsCorrectPriority(): void {
    $events = ApiTokenPathTranslatorSubscriber::getSubscribedEvents();

    self::assertArrayHasKey(PathTranslatorEvent::TRANSLATE, $events);
    self::assertSame('onPathTranslation', $events[PathTranslatorEvent::TRANSLATE][0]);
    self::assertSame(90, $events[PathTranslatorEvent::TRANSLATE][1]);
  }

  /**
   * Builds a mock database connection that returns the given space ID.
   */
  private function buildDb(?int $spaceId): Connection {
    $stmt = $this->createMock(StatementInterface::class);
    $stmt->method('fetchField')->willReturn($spaceId ?? FALSE);

    $query = $this->createMock(SelectInterface::class);
    $query->method('fields')->willReturnSelf();
    $query->method('condition')->willReturnSelf();
    $query->method('range')->willReturnSelf();
    $query->method('execute')->willReturn($stmt);

    $db = $this->createMock(Connection::class);
    $db->method('select')->willReturn($query);

    return $db;
  }

  /**
   * Builds a mock PathTranslatorEvent with the given auth header.
   */
  private function buildEvent(string $path, ?string $authHeader, bool $alreadyResolved = FALSE): PathTranslatorEvent {
    $request = Request::create('/router/translate-path', 'GET', ['path' => $path]);
    if ($authHeader !== NULL) {
      $request->headers->set('Authorization', $authHeader);
    }
    $request->attributes->set('_nodehive_space_resolved', $alreadyResolved);

    $event = $this->getMockBuilder(PathTranslatorEvent::class)
      ->disableOriginalConstructor()
      ->onlyMethods(['getRequest', 'setPath', 'getPath', 'getResponse'])
      ->getMock();
    $event->method('getRequest')->willReturn($request);
    $event->method('getPath')->willReturn($path);
    $metadata = $this->createMock(CacheableMetadata::class);
    $metadata->method('addCacheContexts')->willReturnSelf();
    $metadata->method('addCacheTags')->willReturnSelf();
    $response = $this->createMock(CacheableJsonResponse::class);
    $response->method('getCacheableMetadata')->willReturn($metadata);
    $event->method('getResponse')->willReturn($response);

    return $event;
  }

}
