<?php

declare(strict_types=1);

namespace Drupal\Tests\visitors\Unit\Plugin\views\sort;

use Drupal\Tests\UnitTestCase;
use Drupal\views\Plugin\views\query\Sql;
use Drupal\views\ViewExecutable;
use Drupal\visitors\Plugin\views\sort\NumberRange;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Unit tests for NumberRange views sort plugin.
 *
 * @group visitors
 * @coversDefaultClass \Drupal\visitors\Plugin\views\sort\NumberRange
 */
class NumberRangeTest extends UnitTestCase {

  /**
   * The sort plugin instance.
   *
   * @var \Drupal\visitors\Plugin\views\sort\NumberRange
   */
  protected NumberRange $sort;

  /**
   * The mock query.
   *
   * @var \Drupal\views\Plugin\views\query\Sql|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $query;

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

    $container = new ContainerBuilder();

    $string_translation = $this->getStringTranslationStub();
    $container->set('string_translation', $string_translation);

    // Mock the module handler service.
    $module_handler = $this->createMock('Drupal\Core\Extension\ModuleHandlerInterface');
    $container->set('module_handler', $module_handler);

    \Drupal::setContainer($container);

    $configuration = [
      'table' => 'visitors_visit',
      'field' => 'total_page_views',
    ];
    $plugin_id = 'visitors_number_range';
    $plugin_definition = [];
    $this->sort = NumberRange::create($container, $configuration, $plugin_id, $plugin_definition);

    // Mock the query.
    $this->query = $this->createMock(Sql::class);
    $this->sort->query = $this->query;
    $this->sort->tableAlias = 'vv';
    $this->sort->realField = 'total_page_views';

    // Mock the view.
    $view = $this->createMock(ViewExecutable::class);
    $this->sort->view = $view;

    // Set default options to prevent undefined array key warnings.
    $this->sort->options = [
      'ranges' => "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+",
      'order' => 'ASC',
      'admin_label' => '',
      'exclude' => FALSE,
      'id' => '',
      'relationship' => 'none',
      'group_type' => 'group',
      'ui_name' => '',
    ];
  }

  /**
   * Tests the defineOptions method.
   *
   * @covers ::defineOptions
   */
  public function testDefineOptions(): void {
    $options = $this->sort->defineOptions();

    $this->assertArrayHasKey('ranges', $options);
    $this->assertEquals("0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+", $options['ranges']['default']);
  }

  /**
   * Tests the buildOptionsForm method.
   *
   * @covers ::buildOptionsForm
   */
  public function testBuildOptionsForm(): void {
    $form = [];
    $form_state = $this->createMock('Drupal\Core\Form\FormStateInterface');

    $this->sort->buildOptionsForm($form, $form_state);

    $this->assertArrayHasKey('ranges', $form);
    $this->assertEquals('textarea', $form['ranges']['#type']);
    $this->assertEquals("0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+", $form['ranges']['#default_value']);
  }

  /**
   * Tests the getTotalRanges method with empty ranges.
   *
   * @covers ::getTotalRanges
   */
  public function testGetTotalRangesWithEmptyRanges(): void {
    $this->sort->options['ranges'] = '';
    $this->assertEquals(0, $this->sort->getTotalRanges());

    $this->sort->options['ranges'] = "  \n  \n  ";
    $this->assertEquals(0, $this->sort->getTotalRanges());
  }

  /**
   * Tests the getTotalRanges method with valid ranges.
   *
   * @covers ::getTotalRanges
   */
  public function testGetTotalRangesWithValidRanges(): void {
    $this->sort->options['ranges'] = "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+";
    $this->assertEquals(11, $this->sort->getTotalRanges());
  }

  /**
   * Tests the query method when field already exists.
   *
   * @covers ::query
   */
  public function testQueryWhenFieldExists(): void {
    $this->sort->options['ranges'] = "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+";

    // Mock the query fields to simulate that our target field already exists.
    $this->query->fields = [
      [
        'alias' => 'vv_total_page_views__range',
        'field' => 'some_field',
      ],
    ];

    $this->query->expects($this->once())
      ->method('addOrderBy')
      ->with(
        NULL,
        NULL,
        'ASC',
        'vv_total_page_views__range'
      );

    $this->sort->query();
  }

  /**
   * Tests the query method when field doesn't exist.
   *
   * @covers ::query
   */
  public function testQueryWhenFieldDoesNotExist(): void {
    $this->sort->options['ranges'] = "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+";

    // Mock the query fields to simulate that our target field doesn't exist.
    $this->query->fields = [
      [
        'alias' => 'other_field',
        'field' => 'some_other_field',
      ],
    ];

    $this->query->expects($this->once())
      ->method('addOrderBy')
      ->with(
        NULL,
        $this->stringContains('CASE WHEN vv.total_page_views = 0 THEN 0'),
        'ASC',
        'vv_total_page_views__range'
      );

    $this->sort->query();
  }

  /**
   * Tests the query method with various range formats.
   *
   * @covers ::query
   */
  public function testQueryWithVariousRangeFormats(): void {
    $this->sort->options['ranges'] = "0\n5-10\n15+\n20";

    // Mock the query fields to simulate that our target field doesn't exist.
    $this->query->fields = [
      [
        'alias' => 'other_field',
        'field' => 'some_other_field',
      ],
    ];

    $this->query->expects($this->once())
      ->method('addOrderBy')
      ->with(
        NULL,
        $this->stringContains('CASE WHEN vv.total_page_views = 0 THEN 0'),
        'ASC',
        'vv_total_page_views__range'
      );

    $this->sort->query();
  }

  /**
   * Tests the query method with empty ranges.
   *
   * @covers ::query
   */
  public function testQueryWithEmptyRanges(): void {
    $this->sort->options['ranges'] = '';

    // Mock the query fields to simulate that our target field doesn't exist.
    $this->query->fields = [
      [
        'alias' => 'other_field',
        'field' => 'some_other_field',
      ],
    ];

    $this->query->expects($this->once())
      ->method('addOrderBy')
      ->with(
        NULL,
        'CASE  ELSE 9999 END',
        'ASC',
        'vv_total_page_views__range'
      );

    $this->sort->query();
  }

  /**
   * Tests the query method when field exists with different alias format.
   *
   * @covers ::query
   */
  public function testQueryWhenFieldExistsWithDifferentAliasFormat(): void {
    $this->sort->options['ranges'] = "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+";

    // Mock the query fields to simulate that our target field exists
    // with different field info.
    $this->query->fields = [
      [
        'alias' => 'vv_total_page_views__range',
        'field' => 'vv.total_page_views',
        'table' => 'visitors_visit',
      ],
    ];

    $this->query->expects($this->once())
      ->method('addOrderBy')
      ->with(
        NULL,
        NULL,
        'ASC',
        'vv_total_page_views__range'
      );

    $this->sort->query();
  }

  /**
   * Tests the query method when field exists but alias is missing.
   *
   * @covers ::query
   */
  public function testQueryWhenFieldExistsButAliasMissing(): void {
    $this->sort->options['ranges'] = "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+";

    // Mock the query fields to simulate that our target field
    // exists but without alias.
    $this->query->fields = [
      [
        'field' => 'vv.total_page_views',
        'table' => 'visitors_visit',
      ],
    ];

    $this->query->expects($this->once())
      ->method('addOrderBy')
      ->with(
        NULL,
        $this->stringContains('CASE WHEN vv.total_page_views = 0 THEN 0'),
        'ASC',
        'vv_total_page_views__range'
      );

    $this->sort->query();
  }

  /**
   * Tests the postExecute method with various data.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithVariousData(): void {
    $this->sort->options['ranges'] = "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+";

    $result = [
      (object) ['vv_total_page_views__range' => '1'],
      (object) ['vv_total_page_views__range' => '3'],
      (object) ['vv_total_page_views__range' => '5'],
      (object) ['vv_total_page_views__range' => '7'],
      (object) ['vv_total_page_views__range' => '10'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values, so the result will be
    // larger.
    $this->assertGreaterThan(5, count($result));
  }

  /**
   * Tests the postExecute method with empty values.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithEmptyValues(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $result = [];

    $this->sort->postExecute($result);

    $this->assertEmpty($result);
  }

  /**
   * Tests the postExecute method with single value.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithSingleValue(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $result = [
      (object) ['vv_total_page_views__range' => '1'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values 0 and 2.
    $this->assertGreaterThan(1, count($result));
  }

  /**
   * Tests the postExecute method with large number of ranges.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithLargeNumberOfRanges(): void {
    $ranges = [];
    for ($i = 0; $i < 100; $i++) {
      $ranges[] = (string) $i;
    }
    $this->sort->options['ranges'] = implode("\n", $ranges);

    $result = [
      (object) ['vv_total_page_views__range' => '50'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values from 0 to 99.
    $this->assertGreaterThan(1, count($result));
  }

  /**
   * Tests the postExecute method with non-sequential data.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithNonSequentialData(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $result = [
      (object) ['vv_total_page_views__range' => '2'],
      (object) ['vv_total_page_views__range' => '0'],
      (object) ['vv_total_page_views__range' => '1'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values,
    // but since we have all 3, it should be 3.
    $this->assertGreaterThanOrEqual(3, count($result));
  }

  /**
   * Tests the postExecute method with out-of-range values.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithOutOfRangeValues(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $result = [
      (object) ['vv_total_page_views__range' => '999'],
      (object) ['vv_total_page_views__range' => '0'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values 1 and 2.
    $this->assertGreaterThan(2, count($result));
  }

  /**
   * Tests the postExecute method with mixed data types.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithMixedDataTypes(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $result = [
      (object) ['vv_total_page_views__range' => '1'],
      (object) ['vv_total_page_views__range' => '2'],
      (object) ['vv_total_page_views__range' => '0'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values,
    // but since we have all 3, it should be 3.
    $this->assertGreaterThanOrEqual(3, count($result));
  }

  /**
   * Tests the postExecute method with missing field.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithMissingField(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $result = [
      (object) ['other_field' => 'value', 'vv_total_page_views__range' => NULL],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values 0, 1, and 2.
    $this->assertGreaterThan(1, count($result));
  }

  /**
   * Tests the postExecute method with null values.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithNullValues(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $result = [
      (object) ['vv_total_page_views__range' => NULL],
      (object) ['vv_total_page_views__range' => '1'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values 0 and 2.
    $this->assertGreaterThan(2, count($result));
  }

  /**
   * Tests the postExecute method with complex ranges.
   *
   * @covers ::postExecute
   */
  public function testPostExecuteWithComplexRanges(): void {
    $this->sort->options['ranges'] = "0\n1-5\n6-10\n11-15\n16-20\n21+";

    $result = [
      (object) ['vv_total_page_views__range' => '0'],
      (object) ['vv_total_page_views__range' => '1'],
      (object) ['vv_total_page_views__range' => '2'],
      (object) ['vv_total_page_views__range' => '3'],
      (object) ['vv_total_page_views__range' => '4'],
    ];

    $this->sort->postExecute($result);

    // SequenceService::integer fills in missing values 5.
    $this->assertGreaterThan(5, count($result));
  }

  /**
   * Tests that ensureMyTable is called.
   *
   * @covers ::query
   */
  public function testEnsureMyTableIsCalled(): void {
    $this->sort->options['ranges'] = "0\n1\n2";

    $this->query->expects($this->once())
      ->method('addOrderBy')
      ->with(
        NULL,
        $this->stringContains('CASE WHEN vv.total_page_views = 0 THEN 0'),
        'ASC',
        'vv_total_page_views__range',
        []
      );

    $this->sort->query();
  }

}
