<?php

namespace Drupal\Tests\visitors\Unit\Controller;

use Drupal\Tests\UnitTestCase;
use Drupal\visitors\Controller\VisitorsController;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\ServerBag;

/**
 * Unit tests for the HitDetails controller.
 *
 * @coversDefaultClass \Drupal\visitors\Controller\VisitorsController
 * @uses \Drupal\visitors\Controller\VisitorsController
 * @uses \Drupal\visitors\Helper\VisitorsUrl
 * @group visitors
 */
class VisitorsControllerTest extends UnitTestCase {

  /**
   * The Visitors controller under test.
   *
   * @var \Drupal\visitors\Controller\VisitorsController
   */
  protected $controller;

  /**
   * The visitors tracker.
   *
   * @var \Drupal\visitors\VisitorsTrackerInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $visitorsTracker;

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

  /**
   * The date time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $dateTime;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $logger;

  /**
   * The counter.
   *
   * @var \Drupal\visitors\VisitorsCounterInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $counter;

  /**
   * The cookie.
   *
   * @var \Drupal\visitors\VisitorsCookieInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $cookie;

  /**
   * The device.
   *
   * @var \Drupal\visitors\VisitorsDeviceInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $device;

  /**
   * The location.
   *
   * @var \Drupal\visitors\VisitorsLocationInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $location;

  /**
   * The search engine service.
   *
   * @var \Drupal\visitors\VisitorsSearchEngineInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $searchEngine;

  /**
   * The social networks service.
   *
   * @var \Drupal\visitors\VisitorsSocialNetworksInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $socialNetworksService;

  /**
   * The AI assistants service.
   *
   * @var \Drupal\visitors\VisitorsAiAssistantsInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $aiAssistantsService;

  /**
   * The campaign service.
   *
   * @var \Drupal\visitors\VisitorsCampaignInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $campaignService;

  /**
   * The settings.
   *
   * @var \Drupal\Core\Config\ImmutableConfig|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $settings;

  /**
   * The spam service mock.
   *
   * @var \Drupal\visitors\VisitorsSpamInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $spam;

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

    $container = new ContainerBuilder();

    $this->visitorsTracker = $this->createMock('\Drupal\visitors\VisitorsTrackerInterface');
    $container->set('visitors.tracker', $this->visitorsTracker);

    $this->configFactory = $this->createMock('\Drupal\Core\Config\ConfigFactoryInterface');
    $container->set('config.factory', $this->configFactory);

    $this->dateTime = $this->createMock('\Drupal\Component\Datetime\TimeInterface');
    $container->set('datetime.time', $this->dateTime);

    $this->logger = $this->createMock('\Psr\Log\LoggerInterface');
    $container->set('logger.channel.visitors', $this->logger);

    $this->counter = $this->createMock('\Drupal\visitors\VisitorsCounterInterface');
    $container->set('visitors.counter', $this->counter);

    $this->cookie = $this->createMock('\Drupal\visitors\VisitorsCookieInterface');
    $container->set('visitors.cookie', $this->cookie);

    $this->device = $this->createMock('\Drupal\visitors\VisitorsDeviceInterface');
    $container->set('visitors.device', $this->device);

    $this->location = $this->createMock('\Drupal\visitors\VisitorsLocationInterface');
    $container->set('visitors.location', $this->location);

    $this->searchEngine = $this->createMock('\Drupal\visitors\VisitorsSearchEngineInterface');
    $container->set('visitors.search_engine', $this->searchEngine);

    $this->spam = $this->createMock('\Drupal\visitors\VisitorsSpamInterface');
    $container->set('visitors.spam', $this->spam);

    $this->socialNetworksService = $this->createMock('\Drupal\visitors\VisitorsSocialNetworksInterface');
    $container->set('visitors.social_networks', $this->socialNetworksService);

    $this->aiAssistantsService = $this->createMock('\Drupal\visitors\VisitorsAiAssistantsInterface');
    $container->set('visitors.ai_assistants', $this->aiAssistantsService);

    $this->campaignService = $this->createMock('\Drupal\visitors\VisitorsCampaignInterface');
    $container->set('visitors.campaign', $this->campaignService);

    $this->settings = $this->createMock('\Drupal\Core\Config\ImmutableConfig');
    $this->configFactory->method('get')
      ->with('visitors.settings')
      ->willReturn($this->settings);

    \Drupal::setContainer($container);

    $this->controller = VisitorsController::create($container);
  }

  /**
   * Tests the create() method.
   *
   * @covers ::create
   */
  public function testCreate() {
    $container = \Drupal::getContainer();
    $controller = VisitorsController::create($container);
    $this->assertInstanceOf(VisitorsController::class, $controller);
  }

  /**
   * Tests the construct() method.
   *
   * @covers ::__construct
   */
  public function testConstruct() {

    $controller = new VisitorsController(
      $this->configFactory,
      $this->dateTime,
      $this->logger,
      $this->counter,
      $this->cookie,
      $this->device,
      $this->location,
      $this->visitorsTracker,
      $this->searchEngine,
      $this->spam,
      $this->socialNetworksService,
      $this->aiAssistantsService,
      $this->campaignService,
    );
    $this->assertInstanceOf(VisitorsController::class, $controller);
  }

  /**
   * Tests the track() method.
   *
   * @covers ::track
   * @covers ::getImageContent
   * @covers ::getResponse
   * @covers ::doUrl
   * @covers ::doVisitorId
   * @covers ::doReferrer
   * @covers ::doDeviceDetect
   * @covers ::doCustom
   * @covers ::doCounter
   * @covers ::doConfig
   * @covers ::doPerformance
   * @covers ::doLocalTime
   * @covers ::doLanguage
   * @covers ::doLocation
   * @covers ::doRefererFields
   * @covers ::getDefaultFields
   */
  public function testTrack() {
    $this->dateTime->expects($this->once())
      ->method('getRequestTime')
      ->willReturn(1234567890);
    $request = $this->createMock('\Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getClientIp')
      ->willReturn('127.0.0.1');
    $query = new InputBag();
    $query->set('h', '1');
    $query->set('m', '2');
    $query->set('s', '3');
    $query->set('url', 'https://example.com/page1');
    $query->set('urlref', 'https://google.com/search?q=test');
    $_cvar = [
      ['route', 'entity.node.canonical'],
      ['path', '/node/1'],
      ['server', 'localhost'],
      ['viewed', 'node:1'],
    ];
    $query->set('cvar', json_encode($_cvar));
    $request->server = new ServerBag();
    $request->server->set('HTTP_USER_AGENT', 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GoogleBot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36');
    $request->method('getLanguages')->willReturn(['en_US']);
    $request->query = $query;

    $this->settings->expects($this->once())
      ->method('get')
      ->with('bot_retention_log')
      ->willReturn(0);

    $this->visitorsTracker->expects($this->once())
      ->method('getVisitId')
      ->willReturn(1);

    $this->visitorsTracker->expects($this->once())
      ->method('updateVisit')
      ->willReturn(1);

    $this->device->expects($this->once())
      ->method('hasLibrary')
      ->willReturn(TRUE);

    $this->location->expects($this->once())
      ->method('isValidCountryCode')
      ->with('US')
      ->willReturn(TRUE);
    $this->location->expects($this->once())
      ->method('getContinent')
      ->with('US')
      ->willReturn('NA');

    $response = $this->controller->track($request);
    $this->assertSame('', $response->getContent());
    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

  }

  /**
   * Tests the track() method.
   *
   * @covers ::track
   * @covers ::getImageContent
   * @covers ::getResponse
   * @covers ::doUrl
   * @covers ::doVisitorId
   * @covers ::doReferrer
   * @covers ::doDeviceDetect
   * @covers ::doCustom
   * @covers ::doEvent
   * @covers ::doCounter
   * @covers ::doConfig
   * @covers ::doPerformance
   * @covers ::doLocalTime
   * @covers ::doLanguage
   * @covers ::doLocation
   * @covers ::doRefererFields
   * @covers ::getDefaultFields
   */
  public function testTrackEntityPlugin() {
    $this->dateTime->expects($this->once())
      ->method('getRequestTime')
      ->willReturn(1234567890);
    $request = $this->createMock('\Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getClientIp')
      ->willReturn('127.0.0.1');
    $query = new InputBag();
    $query->set('h', '1');
    $query->set('m', '2');
    $query->set('s', '3');
    $cvar = [
      ['route', 'entity.node.canonical'],
      ['path', '/node/1'],
      ['server', 'localhost'],
      ['viewed', 'node:1'],
    ];
    $e_cvar = [
      ['plugin', 'entity'],
      ['event', 'edit_form'],
      ['plugin_var_1', 'node'],
      ['plugin_var_2', 'article'],
      ['plugin_var_3', 'node_form'],
      ['plugin_var_4', 'node_article_form'],
      ['plugin_int_1', 1],
      ['plugin_int_2', 2],
    ];
    $query->set('cvar', json_encode($cvar));
    $query->set('e_cvar', json_encode($e_cvar));
    $request->server = new ServerBag();
    $request->server->set('HTTP_USER_AGENT', 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GoogleBot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36');
    $request->method('getLanguages')->willReturn(['en_US']);
    $request->query = $query;

    $this->settings->expects($this->once())
      ->method('get')
      ->with('bot_retention_log')
      ->willReturn(0);

    $this->visitorsTracker->expects($this->once())
      ->method('getVisitId')
      ->willReturn(1);

    $this->visitorsTracker->expects($this->once())
      ->method('updateVisit')
      ->willReturn(1);

    $this->device->expects($this->once())
      ->method('hasLibrary')
      ->willReturn(TRUE);

    $this->location->expects($this->once())
      ->method('isValidCountryCode')
      ->with('US')
      ->willReturn(TRUE);
    $this->location->expects($this->once())
      ->method('getContinent')
      ->with('US')
      ->willReturn('NA');

    $response = $this->controller->track($request);
    $this->assertSame('', $response->getContent());
    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

  }

  /**
   * Tests the track() method.
   *
   * @covers ::track
   * @covers ::getImageContent
   * @covers ::getResponse
   * @covers ::doUrl
   * @covers ::doVisitorId
   * @covers ::doReferrer
   * @covers ::doDeviceDetect
   * @covers ::doCustom
   * @covers ::doCounter
   * @covers ::doConfig
   * @covers ::doPerformance
   * @covers ::doLocalTime
   * @covers ::doLanguage
   * @covers ::doLocation
   * @covers ::doRefererFields
   * @covers ::getDefaultFields
   */
  public function testTrackBot() {
    $this->device->expects($this->once())
      ->method('hasLibrary')
      ->willReturn(TRUE);
    $this->device->expects($this->once())
      ->method('doDeviceFields')
      ->willReturnCallback(function (array &$fields, string $user_agent, ?array $server = NULL): void {
        $fields['bot'] = TRUE;
      });
    $request = $this->createMock('\Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getClientIp')
      ->willReturn('127.0.0.1');
    $query = new InputBag();
    $query->set('h', '1');
    $query->set('m', '2');
    $query->set('s', '3');
    $_cvar = [
      ['route', 'entity.node.canonical'],
      ['path', '/node/1'],
      ['server', 'localhost'],
      ['viewed', 'node:1'],
    ];
    $query->set('cvar', json_encode($_cvar));
    $request->server = new ServerBag();
    $request->server->set('HTTP_USER_AGENT', 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GoogleBot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36');
    $request->method('getLanguages')->willReturn([]);
    $request->query = $query;

    $this->settings->expects($this->once())
      ->method('get')
      ->with('bot_retention_log')
      ->willReturn(-1);

    $response = $this->controller->track($request);
    $this->assertSame('', $response->getContent());
    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

  }

  /**
   * Tests the track() method.
   *
   * @covers ::track
   * @covers ::getImageContent
   * @covers ::getResponse
   * @covers ::doUrl
   * @covers ::doVisitorId
   * @covers ::doReferrer
   * @covers ::doDeviceDetect
   * @covers ::doCustom
   * @covers ::doCounter
   * @covers ::doConfig
   * @covers ::doPerformance
   * @covers ::doLocalTime
   * @covers ::doLanguage
   * @covers ::doLocation
   * @covers ::doRefererFields
   * @covers ::getDefaultFields
   */
  public function testTrackEmptyLanguage() {
    $this->dateTime->expects($this->once())
      ->method('getRequestTime')
      ->willReturn(1234567890);
    $request = $this->createMock('\Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getClientIp')
      ->willReturn('127.0.0.1');
    $query = new InputBag();
    $query->set('h', '1');
    $query->set('m', '2');
    $query->set('s', '3');
    $_cvar = [
      ['route', 'entity.node.canonical'],
      ['path', '/node/1'],
      ['server', 'localhost'],
      ['viewed', 'node:1'],
    ];
    $query->set('cvar', json_encode($_cvar));
    $request->server = new ServerBag();
    $request->server->set('HTTP_USER_AGENT', 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GoogleBot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36');
    $request->method('getLanguages')->willReturn([]);
    $request->query = $query;

    $this->settings->expects($this->once())
      ->method('get')
      ->with('bot_retention_log')
      ->willReturn(0);

    $this->visitorsTracker->expects($this->once())
      ->method('getVisitId')
      ->willReturn(1);

    $this->visitorsTracker->expects($this->once())
      ->method('updateVisit')
      ->willReturn(1);

    $response = $this->controller->track($request);
    $this->assertSame('', $response->getContent());
    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

  }

  /**
   * Tests the track() method.
   *
   * @covers ::doLocalTime
   */
  public function testTrackNoLocal() {
    $this->dateTime->expects($this->once())
      ->method('getRequestTime')
      ->willReturn(1234567890);
    $request = $this->createMock('\Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getClientIp')
      ->willReturn('127.0.0.1');
    $query = new InputBag();
    $_cvar = [
      ['route', 'entity.node.canonical'],
      ['path', '/node/1'],
      ['server', 'localhost'],
      ['viewed', 'node:1'],
    ];
    $query->set('cvar', json_encode($_cvar));
    $request->server = new ServerBag();

    $request->method('getLanguages')->willReturn([]);
    $request->query = $query;

    $this->settings->expects($this->once())
      ->method('get')
      ->with('bot_retention_log')
      ->willReturn(0);

    $response = $this->controller->track($request);
    $this->assertSame('', $response->getContent());
    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

  }

  /**
   * Tests location with geoip; location null.
   *
   * @covers ::doLocation
   */
  public function testTrackGeoIp() {
    $this->dateTime->expects($this->once())
      ->method('getRequestTime')
      ->willReturn(1234567890);
    $container = \Drupal::getContainer();

    $continent = new \stdClass();
    $continent->code = 'NA';
    $country = new \stdClass();
    $country->isoCode = 'US';
    $location = new \stdClass();
    $location->latitude = 37.751;
    $location->longitude = -97.822;
    $location->metroCode = '815';
    $subdivision = new \stdClass();
    $subdivision->isoCode = 'CA';
    $postal = new \stdClass();
    $postal->code = '94043';
    $c = new \stdClass();
    $c->names = ['en' => 'Chicago'];

    $city = new \stdClass();
    $city->continent = $continent;
    $city->country = $country;
    $city->subdivisions = [$subdivision];
    $city->city = $c;
    $city->postal = $postal;
    $city->location = $location;

    $geoip = $this->createMock('\Drupal\maxmind_geoip\MaxMindGeoIpInterface');
    $geoip->expects($this->once())
      ->method('city')
      ->with('127.0.0.1')
      ->willReturn($city);
    $container->set('maxmind_geoip.lookup', $geoip);

    $controller = VisitorsController::create($container);

    $request = $this->createMock('\Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getClientIp')
      ->willReturn('127.0.0.1');

    $query = new InputBag();
    $query->set('h', '1');
    $query->set('m', '2');
    $query->set('s', '3');
    $_cvar = [
      ['route', 'entity.node.canonical'],
      ['path', '/node/1'],
      ['server', 'localhost'],
      ['viewed', 'node:1'],
    ];
    $query->set('cvar', json_encode($_cvar));
    $request->server = new ServerBag();

    $request->method('getLanguages')->willReturn([]);
    $request->query = $query;

    $this->settings->expects($this->once())
      ->method('get')
      ->with('bot_retention_log')
      ->willReturn(0);

    $response = $controller->track($request);
    $this->assertSame('', $response->getContent());
    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

  }

  /**
   * Tests location with geoip; location null.
   *
   * @covers ::doLocation
   */
  public function testTrackGeoIpNull() {
    $this->dateTime->expects($this->once())
      ->method('getRequestTime')
      ->willReturn(1234567890);
    $container = \Drupal::getContainer();

    $geoip = $this->createMock('\Drupal\maxmind_geoip\MaxMindGeoIpInterface');
    $container->set('maxmind_geoip.lookup', $geoip);

    $controller = VisitorsController::create($container);

    $request = $this->createMock('\Symfony\Component\HttpFoundation\Request');
    $request->expects($this->once())
      ->method('getClientIp')
      ->willReturn('127.0.0.1');

    $query = new InputBag();
    $query->set('h', '1');
    $query->set('m', '2');
    $query->set('s', '3');
    $_cvar = [
      ['route', 'entity.node.canonical'],
      ['path', '/node/1'],
      ['server', 'localhost'],
      ['viewed', 'node:1'],
    ];
    $query->set('cvar', json_encode($_cvar));
    $request->server = new ServerBag();

    $request->method('getLanguages')->willReturn([]);
    $request->query = $query;

    $this->settings->expects($this->once())
      ->method('get')
      ->with('bot_retention_log')
      ->willReturn(0);

    $response = $controller->track($request);
    $this->assertSame('', $response->getContent());
    $this->assertSame(204, $response->getStatusCode());
    $this->assertSame('must-revalidate, no-cache, no-store, private', $response->headers->get('Cache-Control'));
    $this->assertSame('no-cache', $response->headers->get('Pragma'));
    $this->assertSame('0', $response->headers->get('Expires'));

  }

  /**
   * Tests the doRefererFields() method with direct traffic (no referrer).
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsDirectTraffic(): void {
    $fields = [];
    $query = [];

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('direct', $fields['referer_type']);
    $this->assertEquals('', $fields['referer_url']);
    $this->assertEquals('', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with internal referrer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsInternalReferrer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://example.com/page2',
    ];

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('internal', $fields['referer_type']);
    $this->assertEquals('https://example.com/page2', $fields['referer_url']);
    $this->assertEquals('example.com', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with external referrer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsExternalReferrer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://google.com/search?q=test',
    ];

    // Mock search engine to return null (no match)
    $this->searchEngine->expects($this->once())
      ->method('match')
      ->with('https://google.com/search?q=test')
      ->willReturn(NULL);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('website', $fields['referer_type']);
    $this->assertEquals('https://google.com/search?q=test', $fields['referer_url']);
    $this->assertEquals('google.com', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with empty referrer URL.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsEmptyReferrer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => '',
    ];

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('direct', $fields['referer_type']);
    $this->assertEquals('', $fields['referer_url']);
    $this->assertEquals('', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with malformed referrer URL.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsMalformedReferrer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'not-a-valid-url',
    ];

    // Mock search engine to return null (no match)
    $this->searchEngine->expects($this->once())
      ->method('match')
      ->with('not-a-valid-url')
      ->willReturn(NULL);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('website', $fields['referer_type']);
    $this->assertEquals('not-a-valid-url', $fields['referer_url']);
    $this->assertEmpty($fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with missing URL parameter.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsMissingUrl(): void {
    $fields = [];
    $query = [
      'urlref' => 'https://google.com/search?q=test',
    ];

    // Mock search engine to return null (no match)
    $this->searchEngine->expects($this->once())
      ->method('match')
      ->with('https://google.com/search?q=test')
      ->willReturn(NULL);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('website', $fields['referer_type']);
    $this->assertEquals('https://google.com/search?q=test', $fields['referer_url']);
    $this->assertEquals('google.com', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with search engine referrer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsSearchEngineReferrer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://www.google.com/search?q=drupal+module',
    ];

    // Mock search engine to return a match.
    $searchEngineData = [
      'name' => 'Google',
      'keyword' => 'drupal module',
      'variant' => [
        'urls' => ['google.com'],
        'params' => ['q'],
        'backlink' => 'search?q={k}',
      ],
    ];

    $this->searchEngine->expects($this->once())
      ->method('match')
      ->with('https://www.google.com/search?q=drupal+module')
      ->willReturn($searchEngineData);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('search_engine', $fields['referer_type']);
    $this->assertEquals('https://www.google.com/search?q=drupal+module', $fields['referer_url']);
    $this->assertEquals('Google', $fields['referer_name']);
    $this->assertEquals('drupal module', $fields['referer_keyword']);
  }

  /**
   * Tests the doRefererFields() method with Bing search engine referrer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsBingSearchEngine(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://www.bing.com/search?q=drupal+cms',
    ];

    // Mock search engine to return Bing match.
    $searchEngineData = [
      'name' => 'Bing',
      'keyword' => 'drupal cms',
      'variant' => [
        'urls' => ['bing.com'],
        'params' => ['q'],
        'backlink' => 'search?q={k}',
      ],
    ];

    $this->searchEngine->expects($this->once())
      ->method('match')
      ->with('https://www.bing.com/search?q=drupal+cms')
      ->willReturn($searchEngineData);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('search_engine', $fields['referer_type']);
    $this->assertEquals('https://www.bing.com/search?q=drupal+cms', $fields['referer_url']);
    $this->assertEquals('Bing', $fields['referer_name']);
    $this->assertEquals('drupal cms', $fields['referer_keyword']);
  }

  /**
   * Tests the doRefererFields() method with search engine that has no keyword.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsSearchEngineNoKeyword(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://duckduckgo.com/',
    ];

    // Mock search engine to return match with no keyword.
    $searchEngineData = [
      'name' => 'DuckDuckGo',
      'keyword' => NULL,
      'variant' => [
        'urls' => ['duckduckgo.com'],
        'params' => ['q'],
        'backlink' => '?q={k}',
      ],
    ];

    $this->searchEngine->expects($this->once())
      ->method('match')
      ->with('https://duckduckgo.com/')
      ->willReturn($searchEngineData);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('search_engine', $fields['referer_type']);
    $this->assertEquals('https://duckduckgo.com/', $fields['referer_url']);
    $this->assertEquals('DuckDuckGo', $fields['referer_name']);
    $this->assertNull($fields['referer_keyword']);
  }

  /**
   * Tests the doRefererFields() method with spam referer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsSpamReferer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://spam-site.com/clickbait',
    ];

    // Mock spam service to return true (indicating spam).
    $this->spam->expects($this->once())
      ->method('match')
      ->with('spam-site.com')
      ->willReturn(TRUE);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('spam', $fields['referer_type']);
    $this->assertEquals('https://spam-site.com/clickbait', $fields['referer_url']);
    $this->assertEquals('spam-site.com', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with AI assistant referer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsAiAssistantReferer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://chatgpt.com/chat',
    ];

    // Mock AI assistants service to return ChatGPT match.
    $this->aiAssistantsService->expects($this->once())
      ->method('match')
      ->with('chatgpt.com')
      ->willReturn('ChatGPT');

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('ai_assistant', $fields['referer_type']);
    $this->assertEquals('https://chatgpt.com/chat', $fields['referer_url']);
    $this->assertEquals('ChatGPT', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with another AI assistant referer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsAnotherAiAssistantReferer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://copilot.microsoft.com/chat',
    ];

    // Mock AI assistants service to return Copilot match.
    $this->aiAssistantsService->expects($this->once())
      ->method('match')
      ->with('copilot.microsoft.com')
      ->willReturn('Copilot');

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('ai_assistant', $fields['referer_type']);
    $this->assertEquals('https://copilot.microsoft.com/chat', $fields['referer_url']);
    $this->assertEquals('Copilot', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with social network referer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsSocialNetworkReferer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://facebook.com/post/123',
    ];

    // Mock social networks service to return Facebook match.
    $this->socialNetworksService->expects($this->once())
      ->method('match')
      ->with('facebook.com')
      ->willReturn('Facebook');

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('social_network', $fields['referer_type']);
    $this->assertEquals('https://facebook.com/post/123', $fields['referer_url']);
    $this->assertEquals('Facebook', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with another social network referer.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsAnotherSocialNetworkReferer(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://twitter.com/user/status/456',
    ];

    // Mock social networks service to return Twitter match.
    $this->socialNetworksService->expects($this->once())
      ->method('match')
      ->with('twitter.com')
      ->willReturn('Twitter');

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('social_network', $fields['referer_type']);
    $this->assertEquals('https://twitter.com/user/status/456', $fields['referer_url']);
    $this->assertEquals('Twitter', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with AI assistant that doesn't match.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsAiAssistantNoMatch(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://unknown-ai.com/chat',
    ];

    // Mock AI assistants service to return no match.
    $this->aiAssistantsService->expects($this->once())
      ->method('match')
      ->with('unknown-ai.com')
      ->willReturn(NULL);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    // Should fall back to website type since no AI assistant match.
    $this->assertEquals('website', $fields['referer_type']);
    $this->assertEquals('https://unknown-ai.com/chat', $fields['referer_url']);
    $this->assertEquals('unknown-ai.com', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() method with social network that doesn't match.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsSocialNetworkNoMatch(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://unknown-social.com/post',
    ];

    // Mock social networks service to return no match.
    $this->socialNetworksService->expects($this->once())
      ->method('match')
      ->with('unknown-social.com')
      ->willReturn(NULL);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    // Should fall back to website type since no social network match.
    $this->assertEquals('website', $fields['referer_type']);
    $this->assertEquals('https://unknown-social.com/post', $fields['referer_url']);
    $this->assertEquals('unknown-social.com', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() campaign referer using mtm_* parameters.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsCampaignMtmParameters(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://example.com/offer?mtm_campaign=Best-Seller&mtm_source=Newsletter_7&mtm_medium=email&mtm_keyword=discount&mtm_content=banner',
    ];

    // Mock campaign service to return campaign data.
    $this->campaignService->expects($this->once())
      ->method('parse')
      ->with('https://example.com/offer?mtm_campaign=Best-Seller&mtm_source=Newsletter_7&mtm_medium=email&mtm_keyword=discount&mtm_content=banner')
      ->willReturn([
        'campaign' => 'Best-Seller',
        'source' => 'Newsletter_7',
        'medium' => 'email',
        'keyword' => 'discount',
        'content' => 'banner',
      ]);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('campaign', $fields['referer_type']);
    $this->assertEquals('https://example.com/offer?mtm_campaign=Best-Seller&mtm_source=Newsletter_7&mtm_medium=email&mtm_keyword=discount&mtm_content=banner', $fields['referer_url']);
    $this->assertEquals('Best-Seller', $fields['referer_name']);
    $this->assertEquals('discount', $fields['referer_keyword']);
  }

  /**
   * Tests the doRefererFields() campaign using utm_* parameters.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsCampaignUtmParameters(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://example.com/offer?utm_campaign=Summer_Sale&utm_source=google&utm_medium=cpc&utm_term=summer+sale&utm_content=text_ad',
    ];

    // Mock campaign service to return campaign data.
    $this->campaignService->expects($this->once())
      ->method('parse')
      ->with('https://example.com/offer?utm_campaign=Summer_Sale&utm_source=google&utm_medium=cpc&utm_term=summer+sale&utm_content=text_ad')
      ->willReturn([
        'campaign' => 'Summer_Sale',
        'source' => 'google',
        'medium' => 'cpc',
        'keyword' => 'summer+sale',
        'content' => 'text_ad',
      ]);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('campaign', $fields['referer_type']);
    $this->assertEquals('https://example.com/offer?utm_campaign=Summer_Sale&utm_source=google&utm_medium=cpc&utm_term=summer+sale&utm_content=text_ad', $fields['referer_url']);
    $this->assertEquals('Summer_Sale', $fields['referer_name']);
    $this->assertEquals('summer+sale', $fields['referer_keyword']);
  }

  /**
   * Tests the doRefererFields() method that has no campaign data.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsCampaignNoData(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://example.com/offer?page=1&sort=date',
    ];

    // Mock campaign service to return no campaign data.
    $this->campaignService->expects($this->once())
      ->method('parse')
      ->with('https://example.com/offer?page=1&sort=date')
      ->willReturn(NULL);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    // Should fall back to website type since no campaign data.
    $this->assertEquals('internal', $fields['referer_type']);
    $this->assertEquals('https://example.com/offer?page=1&sort=date', $fields['referer_url']);
    $this->assertEquals('example.com', $fields['referer_name']);
  }

  /**
   * Tests the doRefererFields() with campaign that has partial campaign data.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsCampaignPartialData(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://example.com/offer?mtm_campaign=Holiday_Special',
    ];

    // Mock campaign service to return partial campaign data.
    $this->campaignService->expects($this->once())
      ->method('parse')
      ->with('https://example.com/offer?mtm_campaign=Holiday_Special')
      ->willReturn([
        'campaign' => 'Holiday_Special',
      ]);

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('campaign', $fields['referer_type']);
    $this->assertEquals('https://example.com/offer?mtm_campaign=Holiday_Special', $fields['referer_url']);
    $this->assertEquals('Holiday_Special', $fields['referer_name']);
    $this->assertEquals('', $fields['referer_keyword']);
  }

  /**
   * Tests the doRefererFields() with campaign priority over other services.
   *
   * @covers ::doRefererFields
   */
  public function testDoRefererFieldsCampaignPriority(): void {
    $fields = [];
    $query = [
      'url' => 'https://example.com/page1',
      'urlref' => 'https://facebook.com/post/123?mtm_campaign=Social_Campaign&mtm_source=facebook&mtm_medium=social',
    ];

    // Mock campaign service to return campaign data.
    $this->campaignService->expects($this->once())
      ->method('parse')
      ->with('https://facebook.com/post/123?mtm_campaign=Social_Campaign&mtm_source=facebook&mtm_medium=social')
      ->willReturn([
        'campaign' => 'Social_Campaign',
        'source' => 'facebook',
        'medium' => 'social',
      ]);

    $this->socialNetworksService->expects($this->never())
      ->method('match');

    $reflection = new \ReflectionClass($this->controller);
    $method = $reflection->getMethod('doRefererFields');
    $method->setAccessible(TRUE);
    $method->invokeArgs($this->controller, [&$fields, $query]);

    $this->assertEquals('campaign', $fields['referer_type']);
    $this->assertEquals('Social_Campaign', $fields['referer_name']);
    $this->assertEquals('', $fields['referer_keyword']);
  }

}
