<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\Service\JsonPath;

use Drupal\flowdrop_runtime\Service\JsonPath\JsonPathService;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;

/**
 * Unit tests for JsonPathService.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\JsonPath\JsonPathService
 * @group flowdrop_runtime
 */
class JsonPathServiceTest extends TestCase {

  use ProphecyTrait;

  /**
   * The service under test.
   *
   * @var \Drupal\flowdrop_runtime\Service\JsonPath\JsonPathService
   */
  protected JsonPathService $service;

  /**
   * Sample data for testing.
   *
   * @var array<string, mixed>
   */
  protected array $sampleData;

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

    $logger = $this->prophesize(LoggerInterface::class);
    $this->service = new JsonPathService($logger->reveal());

    // Sample data structure for testing.
    $this->sampleData = [
      "store" => [
        "name" => "Test Store",
        "books" => [
          ["title" => "Book 1", "price" => 10, "category" => "fiction"],
          ["title" => "Book 2", "price" => 15, "category" => "non-fiction"],
          ["title" => "Book 3", "price" => 8, "category" => "fiction"],
        ],
        "electronics" => [
          ["name" => "Laptop", "price" => 1000],
          ["name" => "Phone", "price" => 500],
        ],
      ],
      "users" => [
        ["name" => "Alice", "email" => "alice@example.com", "active" => TRUE],
        ["name" => "Bob", "email" => "bob@example.com", "active" => FALSE],
        ["name" => "Charlie", "email" => "charlie@example.com", "active" => TRUE],
      ],
      "metadata" => [
        "version" => "1.0",
        "timestamp" => 1234567890,
      ],
    ];
  }

  /**
   * @covers ::isJsonPath
   */
  public function testIsJsonPathReturnsTrueForJsonPathExpressions(): void {
    $this->assertTrue($this->service->isJsonPath("$.users"));
    $this->assertTrue($this->service->isJsonPath("$.users[*].name"));
    $this->assertTrue($this->service->isJsonPath("$..email"));
    $this->assertTrue($this->service->isJsonPath("$.store.books[0]"));
    // With leading space.
    $this->assertTrue($this->service->isJsonPath(" $.users"));
  }

  /**
   * @covers ::isJsonPath
   */
  public function testIsJsonPathReturnsFalseForPropertyPaths(): void {
    $this->assertFalse($this->service->isJsonPath("users.0.name"));
    $this->assertFalse($this->service->isJsonPath("[users][0][name]"));
    $this->assertFalse($this->service->isJsonPath("store.books"));
    $this->assertFalse($this->service->isJsonPath(""));
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithJsonPathSingleValue(): void {
    $result = $this->service->extract($this->sampleData, "$.store.name");
    $this->assertEquals("Test Store", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithJsonPathArray(): void {
    $result = $this->service->extract($this->sampleData, "$.users[*].name");
    $this->assertIsArray($result);
    $this->assertCount(3, $result);
    $this->assertEquals(["Alice", "Bob", "Charlie"], $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithJsonPathRecursiveDescent(): void {
    $result = $this->service->extract($this->sampleData, "$..email");
    $this->assertIsArray($result);
    $this->assertCount(3, $result);
    $this->assertContains("alice@example.com", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithJsonPathIndex(): void {
    $result = $this->service->extract($this->sampleData, "$.store.books[0].title");
    $this->assertEquals("Book 1", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithJsonPathNegativeIndex(): void {
    $result = $this->service->extract($this->sampleData, "$.store.books[-1].title");
    $this->assertEquals("Book 3", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithPropertyPath(): void {
    $result = $this->service->extract($this->sampleData, "store.name");
    $this->assertEquals("Test Store", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithBracketNotation(): void {
    $result = $this->service->extract($this->sampleData, "[store][name]");
    $this->assertEquals("Test Store", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithNestedPropertyPath(): void {
    $result = $this->service->extract($this->sampleData, "users.0.name");
    $this->assertEquals("Alice", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractReturnsDefaultForMissingPath(): void {
    $result = $this->service->extract($this->sampleData, "$.nonexistent.path", "default_value");
    $this->assertEquals("default_value", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithEmptyPathReturnsEntireData(): void {
    $result = $this->service->extract($this->sampleData, "");
    $this->assertEquals($this->sampleData, $result);
  }

  /**
   * @covers ::extractFirst
   */
  public function testExtractFirstReturnsSingleValue(): void {
    $result = $this->service->extractFirst($this->sampleData, "$.users[*].name");
    $this->assertEquals("Alice", $result);
  }

  /**
   * @covers ::extractFirst
   */
  public function testExtractFirstReturnsDefaultWhenNoMatch(): void {
    $result = $this->service->extractFirst($this->sampleData, "$.nonexistent", "fallback");
    $this->assertEquals("fallback", $result);
  }

  /**
   * @covers ::extractAll
   */
  public function testExtractAllReturnsArray(): void {
    $result = $this->service->extractAll($this->sampleData, "$.users[*].email");
    $this->assertIsArray($result);
    $this->assertCount(3, $result);
  }

  /**
   * @covers ::extractAll
   */
  public function testExtractAllReturnsEmptyArrayWhenNoMatch(): void {
    $result = $this->service->extractAll($this->sampleData, "$.nonexistent");
    $this->assertIsArray($result);
    $this->assertEmpty($result);
  }

  /**
   * @covers ::exists
   */
  public function testExistsReturnsTrueForExistingPath(): void {
    $this->assertTrue($this->service->exists($this->sampleData, "$.store.name"));
    $this->assertTrue($this->service->exists($this->sampleData, "store.name"));
  }

  /**
   * @covers ::exists
   */
  public function testExistsReturnsFalseForNonArrayData(): void {
    // Scalar data doesn't have property paths.
    $this->assertFalse($this->service->exists("string", "any.path"));
    $this->assertFalse($this->service->exists(123, "some.property"));
    $this->assertFalse($this->service->exists(NULL, "test"));
  }

  /**
   * @covers ::exists
   */
  public function testExistsWithJsonPathForMissingData(): void {
    // JSONPath queries that return empty results.
    $emptyData = ["items" => []];
    $this->assertFalse($this->service->exists($emptyData, "$.items[0]"));
  }

  /**
   * @covers ::applyMappings
   */
  public function testApplyMappingsWithJsonPath(): void {
    // Note: applyMappings uses extractFirst(), so wildcard expressions
    // return only the first match. Use extractAll() for multiple values.
    $mappings = [
      "store_name" => "$.store.name",
      "first_user" => "$.users[0].name",
    // Returns first match only.
      "first_email" => "$.users[*].email",
    ];

    $result = $this->service->applyMappings($this->sampleData, $mappings);

    $this->assertEquals("Test Store", $result["store_name"]);
    $this->assertEquals("Alice", $result["first_user"]);
    // applyMappings uses extractFirst, so wildcard returns first match.
    $this->assertEquals("alice@example.com", $result["first_email"]);
  }

  /**
   * @covers ::applyMappings
   */
  public function testApplyMappingsWithPropertyPaths(): void {
    $mappings = [
      "store_name" => "store.name",
      "version" => "metadata.version",
    ];

    $result = $this->service->applyMappings($this->sampleData, $mappings);

    $this->assertEquals("Test Store", $result["store_name"]);
    $this->assertEquals("1.0", $result["version"]);
  }

  /**
   * @covers ::applyMappings
   */
  public function testApplyMappingsWithLiteralValues(): void {
    $mappings = [
      "static_key" => "literal:my_static_value",
      "quoted_value" => '"quoted string"',
      "single_quoted" => "'single quoted'",
    ];

    $result = $this->service->applyMappings($this->sampleData, $mappings);

    $this->assertEquals("my_static_value", $result["static_key"]);
    $this->assertEquals("quoted string", $result["quoted_value"]);
    $this->assertEquals("single quoted", $result["single_quoted"]);
  }

  /**
   * @covers ::isValidJsonPath
   */
  public function testIsValidJsonPathReturnsTrueForValidExpressions(): void {
    $this->assertTrue($this->service->isValidJsonPath("$.users"));
    $this->assertTrue($this->service->isValidJsonPath("$.users[*].name"));
    $this->assertTrue($this->service->isValidJsonPath("$..email"));
  }

  /**
   * @covers ::isValidJsonPath
   */
  public function testIsValidJsonPathReturnsFalseForInvalidExpressions(): void {
    // Not JSONPath (no $).
    $this->assertFalse($this->service->isValidJsonPath("users.name"));
    // Invalid syntax.
    $this->assertFalse($this->service->isValidJsonPath("$.[invalid"));
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithJsonString(): void {
    $jsonString = '{"name": "Test", "value": 123}';
    $result = $this->service->extract($jsonString, "$.name");
    $this->assertEquals("Test", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithNestedJsonPath(): void {
    $result = $this->service->extract($this->sampleData, "$.store.books[*].price");
    $this->assertIsArray($result);
    $this->assertEquals([10, 15, 8], $result);
  }

  /**
   * @covers ::extract
   * @dataProvider sliceNotationProvider
   *
   * @param string $path
   *   The JSONPath expression.
   * @param array<int, mixed> $expected
   *   Expected result.
   */
  public function testExtractWithSliceNotation(string $path, array $expected): void {
    $data = ["items" => [1, 2, 3, 4, 5]];
    $result = $this->service->extract($data, $path);
    $this->assertEquals($expected, $result);
  }

  /**
   * Data provider for slice notation tests.
   *
   * @return array<string, array{0: string, 1: array<int, int>}>
   *   Test cases.
   */
  public static function sliceNotationProvider(): array {
    return [
      "first three" => ["$.items[0:3]", [1, 2, 3]],
      "last two" => ["$.items[-2:]", [4, 5]],
    ];
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithNullData(): void {
    $result = $this->service->extract(NULL, "$.any.path", "default");
    $this->assertEquals("default", $result);
  }

  /**
   * @covers ::extract
   */
  public function testExtractWithScalarData(): void {
    $result = $this->service->extract("simple string", "$.path", "default");
    $this->assertEquals("default", $result);
  }

  /**
   * @covers ::applyMappings
   */
  public function testApplyMappingsMixedPaths(): void {
    $mappings = [
      "jsonpath_field" => "$.store.name",
      "property_field" => "metadata.version",
      "literal_field" => "literal:static",
    ];

    $result = $this->service->applyMappings($this->sampleData, $mappings);

    $this->assertEquals("Test Store", $result["jsonpath_field"]);
    $this->assertEquals("1.0", $result["property_field"]);
    $this->assertEquals("static", $result["literal_field"]);
  }

}
