<?php

declare(strict_types=1);

namespace Drupal\Tests\visitors\Unit\Service;

use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\visitors\Service\SearchEngineService;

/**
 * Tests for the SearchEngineService.
 *
 * @coversDefaultClass \Drupal\visitors\Service\SearchEngineService
 * @group visitors
 */
final class SearchEngineServiceTest extends UnitTestCase {

  /**
   * The search engine service under test.
   *
   * @var \Drupal\visitors\Service\SearchEngineService
   */
  private SearchEngineService $searchEngineService;

  /**
   * Mock config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  private ConfigFactoryInterface $configFactory;

  /**
   * Mock config object.
   *
   * @var \Drupal\Core\Config\Config|\PHPUnit\Framework\MockObject\MockObject
   */
  private Config $config;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->configFactory = $this->createMock(ConfigFactoryInterface::class);
    $this->config        = $this->createMock(Config::class);

    $this->configFactory
      ->method('get')
      ->with('visitors.search_engines')
      ->willReturn($this->config);

    $this->searchEngineService = new SearchEngineService($this->configFactory);
  }

  /**
   * Tests the constructor.
   *
   * @covers \Drupal\visitors\Service\HostnameMatcherServiceBase::__construct
   */
  public function testConstructor(): void {
    $search_engine_service = new SearchEngineService($this->configFactory);
    $this->assertInstanceOf(SearchEngineService::class, $search_engine_service);
  }

  /**
   * Tests matching Google search URLs.
   *
   * @covers ::match
   * @covers ::matchVariant
   * @covers ::matchHostPattern
   * @covers ::extractKeyword
   */
  public function testMatchGoogle(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'Google',
          'variants' => [
            [
              'urls' => ['google.com', 'www.google.com'],
              'params' => ['q'],
              'backlink' => 'search?q={k}',
            ],
          ],
        ],
      ]);

    $result = $this->searchEngineService->match('https://www.google.com/search?q=drupal');

    $this->assertIsArray($result);
    $this->assertEquals('Google', $result['name']);
    $this->assertEquals('drupal', $result['keyword']);
    $this->assertArrayHasKey('variant', $result);
  }

  /**
   * Tests matching with wildcard patterns.
   *
   * @covers ::match
   * @covers ::matchHostPattern
   */
  public function testMatchWithWildcard(): void {
    $searchEnginesData = [
      [
        'label' => 'Google',
        'variants' => [
          [
            'urls' => ['google.{}', '{}.google.com'],
            'params' => ['q'],
            'backlink' => 'search?q={k}',
          ],
        ],
      ],
    ];

    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn($searchEnginesData);

    $result = $this->searchEngineService->match('https://de.google.com/search?q=test');
    $this->assertIsArray($result);
    $this->assertEquals('Google', $result['name']);
    $this->assertEquals('test', $result['keyword']);

    $result = $this->searchEngineService->match('https://google.de/search?q=test');
    $this->assertIsArray($result);
    $this->assertEquals('Google', $result['name']);
    $this->assertEquals('test', $result['keyword']);
  }

  /**
   * Tests matching with regex parameters.
   *
   * @covers ::match
   * @covers ::extractKeyword
   */
  public function testMatchWithRegexParams(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => '1.cz',
          'variants' => [
            [
              'urls' => ['1.cz'],
              'params' => ['/s\/([^\/]+)/', 'q'],
              'backlink' => 's/{k}',
              'charsets' => ['iso-8859-2'],
            ],
          ],
        ],
      ]);

    $result = $this->searchEngineService->match('https://1.cz/s/drupal');
    $this->assertIsArray($result);
    $this->assertEquals('1.cz', $result['name']);
    $this->assertEquals('drupal', $result['keyword']);
  }

  /**
   * Tests matching with hidden keywords exclusion.
   *
   * @covers ::match
   * @covers ::matchesHiddenKeyword
   */
  public function testMatchWithHiddenKeywords(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'Google',
          'variants' => [
            [
              'urls' => ['google.com'],
              'params' => ['q'],
              'backlink' => 'search?q={k}',
              'hiddenkeyword' => ['/^$/', '/\/search(\?.*)?/', '/\/url\?.*/'],
            ],
          ],
        ],
      ]);

    // Should match normal search
    // (special case: /search with q parameter is allowed)
    $result = $this->searchEngineService->match('https://google.com/search?q=drupal');
    $this->assertIsArray($result);
    $this->assertEquals('Google', $result['name']);

    // Should not match URL redirects (hidden keyword)
    $result = $this->searchEngineService->match('https://google.com/url?sa=t&url=example.com');
    $this->assertNull($result);

    // Should not match search without query parameter.
    $result = $this->searchEngineService->match('https://google.com/search');
    $this->assertNull($result);
  }

  /**
   * Tests non-matching URLs.
   *
   * @covers ::match
   */
  public function testNonMatch(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'Google',
          'variants' => [
            [
              'urls' => ['google.com'],
              'params' => ['q'],
              'backlink' => 'search?q={k}',
            ],
          ],
        ],
      ]);

    // Different domain.
    $result = $this->searchEngineService->match('https://bing.com/search?q=test');
    $this->assertNull($result);

    // Invalid URL.
    $result = $this->searchEngineService->match('not-a-url');
    $this->assertNull($result);

    // No query parameter.
    $result = $this->searchEngineService->match('https://google.com/');
    $this->assertIsArray($result);
    $this->assertEquals('Google', $result['name']);
    $this->assertNull($result['keyword']);
  }

  /**
   * Tests matchVariant method when matchesHiddenKeyword returns TRUE.
   *
   * @covers ::match
   * @covers ::matchVariant
   * @covers ::matchesHiddenKeyword
   */
  public function testMatchVariantWithHiddenKeywordMatch(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'Google',
          'variants' => [
            [
              'urls' => ['google.com'],
              'params' => ['q'],
              'backlink' => 'search?q={k}',
              'hiddenkeyword' => ['/\/url\?.*/', '/\/maps\?.*/'],
            ],
          ],
        ],
      ]);

    // Test URL that matches host pattern but is excluded by hidden keyword.
    $result = $this->searchEngineService->match('https://google.com/url?sa=t&url=example.com');
    $this->assertNull($result, 'URL with hidden keyword should not match');

    // Test another URL with different hidden keyword pattern.
    $result = $this->searchEngineService->match('https://google.com/maps?q=location');
    $this->assertNull($result, 'URL with maps hidden keyword should not match');

    // Test URL that should still match (no hidden keyword)
    $result = $this->searchEngineService->match('https://google.com/search?q=drupal');
    $this->assertIsArray($result, 'URL without hidden keyword should match');
    $this->assertEquals('Google', $result['name']);
    $this->assertEquals('drupal', $result['keyword']);
  }

  /**
   * Tests extractKeyword method when it returns NULL.
   *
   * @covers ::match
   * @covers ::extractKeyword
   */
  public function testExtractKeywordReturnsNull(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'TestEngine',
          'variants' => [
            [
              'urls' => ['test-engine.com'],
              'params' => ['q'],
              'backlink' => 'search?q={k}',
            ],
          ],
        ],
      ]);

    // Test URL with empty query parameter value.
    $result = $this->searchEngineService->match('https://test-engine.com/search?q=');
    $this->assertIsArray($result);
    $this->assertEquals('TestEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when query parameter is empty');

    // Test URL with missing query parameter.
    $result = $this->searchEngineService->match('https://test-engine.com/search?other=value');
    $this->assertIsArray($result);
    $this->assertEquals('TestEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when required parameter is missing');

    // Test URL with no query string at all.
    $result = $this->searchEngineService->match('https://test-engine.com/search');
    $this->assertIsArray($result);
    $this->assertEquals('TestEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when no query string exists');

    // Test URL with empty query string.
    $result = $this->searchEngineService->match('https://test-engine.com/search?');
    $this->assertIsArray($result);
    $this->assertEquals('TestEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when query string is empty');
  }

  /**
   * Tests extractKeyword method with regex parameters that return NULL.
   *
   * @covers ::match
   * @covers ::extractKeyword
   */
  public function testExtractKeywordWithRegexParamsReturnsNull(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'RegexEngine',
          'variants' => [
            [
              'urls' => ['regex-engine.com'],
              'params' => ['/\/search\/([^\/]+)/', '/\/find\/([^\/]+)/'],
              'backlink' => 'search/{k}',
            ],
          ],
        ],
      ]);

    // Test URL that doesn't match any regex pattern.
    // Use root path to match host.
    $result = $this->searchEngineService->match('https://regex-engine.com/');
    $this->assertIsArray($result);
    $this->assertEquals('RegexEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when regex patterns do not match');
  }

  /**
   * Tests extractKeyword method with regex pattern that has no capture group.
   *
   * @covers ::match
   * @covers ::extractKeyword
   */
  public function testExtractKeywordWithRegexNoCaptureGroup(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'NoCaptureEngine',
          'variants' => [
            [
              'urls' => ['no-capture.com'],
    // No capture group.
              'params' => ['/\/search\/.*/'],
              'backlink' => 'search/{k}',
            ],
          ],
        ],
      ]);

    $result = $this->searchEngineService->match('https://no-capture.com/');
    $this->assertIsArray($result);
    $this->assertEquals('NoCaptureEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when regex has no capture group');
  }

  /**
   * Tests extractKeyword method with no parameters defined.
   *
   * @covers ::match
   * @covers ::extractKeyword
   */
  public function testExtractKeywordWithNoParams(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'NoParamsEngine',
          'variants' => [
            [
              'urls' => ['no-params.com'],
              'backlink' => 'search',
              // No 'params' key defined.
            ],
          ],
        ],
      ]);

    $result = $this->searchEngineService->match('https://no-params.com/');
    $this->assertIsArray($result);
    $this->assertEquals('NoParamsEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when no parameters are defined');
  }

  /**
   * Tests extractKeyword method with empty params array.
   *
   * @covers ::match
   * @covers ::extractKeyword
   */
  public function testExtractKeywordWithEmptyParams(): void {
    $this->config
      ->method('get')
      ->with('sites')
      ->willReturn([
        [
          'label' => 'EmptyParamsEngine',
          'variants' => [
            [
              'urls' => ['empty-params.com'],
    // Empty params array.
              'params' => [],
              'backlink' => 'search',
            ],
          ],
        ],
      ]);

    $result = $this->searchEngineService->match('https://empty-params.com/');
    $this->assertIsArray($result);
    $this->assertEquals('EmptyParamsEngine', $result['name']);
    $this->assertNull($result['keyword'], 'Keyword should be NULL when params array is empty');
  }

}
