<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_node_processor\Unit\Plugin\FlowDropNodeProcessor;

use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\JsonToData;
use PHPUnit\Framework\TestCase;

/**
 * Tests the JsonToData node processor.
 *
 * @coversDefaultClass \Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\JsonToData
 * @group flowdrop_node_processor
 */
class JsonToDataTest extends TestCase {

  /**
   * Creates a JsonToData processor instance.
   *
   * @return \Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\JsonToData
   *   The processor instance.
   */
  private function createProcessor(): JsonToData {
    return new JsonToData([], 'json_to_data', ['id' => 'json_to_data']);
  }

  // -----------------------------------------------------------------------
  // Strict mode tests
  // -----------------------------------------------------------------------

  /**
   * Tests strict mode decodes a valid JSON object.
   *
   * @covers ::process
   */
  public function testStrictModeValidJsonObject(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '{"name": "Alice", "age": 30}',
      'parse_mode' => 'strict',
    ]));

    $this->assertTrue($result['success']);
    $this->assertSame('Alice', $result['data']['name']);
    $this->assertSame(30, $result['data']['age']);
  }

  /**
   * Tests strict mode decodes a valid JSON array.
   *
   * @covers ::process
   */
  public function testStrictModeValidJsonArray(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '[1, 2, 3]',
      'parse_mode' => 'strict',
    ]));

    $this->assertTrue($result['success']);
    $this->assertSame([1, 2, 3], $result['data']);
  }

  /**
   * Tests strict mode returns empty array for invalid JSON.
   *
   * The plugin converts malformed JSON to an empty array rather than NULL,
   * so success is true (non-NULL result). Callers should check data emptiness
   * for failed conversions in strict mode.
   *
   * @covers ::process
   */
  public function testStrictModeInvalidJsonReturnsEmptyArray(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => 'not valid json at all',
      'parse_mode' => 'strict',
    ]));

    $this->assertSame([], $result['data']);
  }

  /**
   * Tests strict mode returns empty array for empty string input.
   *
   * @covers ::process
   */
  public function testStrictModeEmptyStringReturnsEmptyArray(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '',
      'parse_mode' => 'strict',
    ]));

    $this->assertSame([], $result['data']);
  }

  /**
   * Tests strict mode passes through an already-decoded array.
   *
   * @covers ::process
   */
  public function testStrictModePassesThroughDecodedArray(): void {
    $processor = $this->createProcessor();
    $input = ['key' => 'value', 'nested' => [1, 2, 3]];
    $result = $processor->process(new ParameterBag([
      'json' => $input,
      'parse_mode' => 'strict',
    ]));

    $this->assertTrue($result['success']);
    $this->assertSame($input, $result['data']);
  }

  /**
   * Tests strict mode does not include parse_source in output.
   *
   * @covers ::process
   */
  public function testStrictModeHasNoParseSource(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '{"x": 1}',
      'parse_mode' => 'strict',
    ]));

    $this->assertArrayNotHasKey('parse_source', $result);
  }

  /**
   * Tests strict mode is the default when parse_mode is omitted.
   *
   * @covers ::process
   */
  public function testStrictModeIsDefault(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '{"default": true}',
    ]));

    $this->assertTrue($result['success']);
    $this->assertArrayNotHasKey('parse_source', $result);
  }

  // -----------------------------------------------------------------------
  // Tolerant mode tests
  // -----------------------------------------------------------------------

  /**
   * Tests tolerant mode decodes direct valid JSON.
   *
   * @covers ::process
   */
  public function testTolerantModeDirectJson(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '{"title": "Hello", "body": "World"}',
      'parse_mode' => 'tolerant',
    ]));

    $this->assertTrue($result['success']);
    $this->assertSame('Hello', $result['data']['title']);
    $this->assertSame('direct', $result['parse_source']);
  }

  /**
   * Tests tolerant mode extracts JSON from a markdown ```json code fence.
   *
   * @covers ::process
   */
  public function testTolerantModeExtractsFromJsonCodeFence(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => "Here is the data:\n\n```json\n{\"title\": \"Extracted\"}\n```\n",
      'parse_mode' => 'tolerant',
    ]));

    $this->assertTrue($result['success']);
    $this->assertSame('Extracted', $result['data']['title']);
    $this->assertSame('code_fence', $result['parse_source']);
  }

  /**
   * Tests tolerant mode extracts JSON from a plain ``` code fence.
   *
   * @covers ::process
   */
  public function testTolerantModeExtractsFromPlainCodeFence(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => "```\n{\"key\": \"value\"}\n```",
      'parse_mode' => 'tolerant',
    ]));

    $this->assertTrue($result['success']);
    $this->assertSame('value', $result['data']['key']);
    $this->assertSame('code_fence', $result['parse_source']);
  }

  /**
   * Tests tolerant mode returns failure when fence contains invalid JSON.
   *
   * @covers ::process
   */
  public function testTolerantModeMalformedFenceReturnsFailure(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => "```json\nnot valid json\n```",
      'parse_mode' => 'tolerant',
    ]));

    $this->assertFalse($result['success']);
    $this->assertNull($result['data']);
    $this->assertNull($result['parse_source']);
  }

  /**
   * Tests tolerant mode includes parse_source in all outputs.
   *
   * @covers ::process
   */
  public function testTolerantModeAlwaysHasParseSource(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => 'not json',
      'parse_mode' => 'tolerant',
    ]));

    $this->assertArrayHasKey('parse_source', $result);
  }

  /**
   * Tests tolerant mode passes through an already-decoded array.
   *
   * @covers ::process
   */
  public function testTolerantModePassesThroughDecodedArray(): void {
    $processor = $this->createProcessor();
    $input = ['already' => 'decoded'];
    $result = $processor->process(new ParameterBag([
      'json' => $input,
      'parse_mode' => 'tolerant',
    ]));

    $this->assertTrue($result['success']);
    $this->assertSame($input, $result['data']);
    $this->assertSame('direct', $result['parse_source']);
  }

  /**
   * Tests tolerant mode with a non-string, non-array input.
   *
   * @covers ::process
   */
  public function testTolerantModeNonStringInputReturnsFailure(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => 42,
      'parse_mode' => 'tolerant',
    ]));

    $this->assertFalse($result['success']);
    $this->assertNull($result['data']);
  }

  /**
   * Tests output schema data type is an array of valid JSON Schema types.
   *
   * @covers ::getOutputSchema
   */
  public function testOutputSchemaDataTypeIsValid(): void {
    $processor = $this->createProcessor();
    $schema = $processor->getOutputSchema();

    $dataType = $schema['properties']['data']['type'];
    $this->assertIsArray($dataType);
    $this->assertContains('object', $dataType);
    $this->assertContains('array', $dataType);
    $this->assertContains('null', $dataType);
    $this->assertNotContains('mixed', $dataType);
  }

}
