<?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 (assert-or-fail: throws on invalid input)
  // -----------------------------------------------------------------------

  /**
   * 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->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->assertSame([1, 2, 3], $result['data']);
  }

  /**
   * Tests strict mode accepts a valid JSON scalar.
   *
   * @covers ::process
   */
  public function testStrictModeAcceptsScalar(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '123',
      'parse_mode' => 'strict',
    ]));

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

  /**
   * Tests strict mode throws on invalid JSON.
   *
   * A parse failure surfaces as a \RuntimeException, which NodeRuntimeService
   * converts to an error-status output for the orchestrator to route or fail.
   *
   * @covers ::process
   */
  public function testStrictModeInvalidJsonThrows(): void {
    $processor = $this->createProcessor();
    $this->expectException(\RuntimeException::class);
    $processor->process(new ParameterBag([
      'json' => 'not valid json at all',
      'parse_mode' => 'strict',
    ]));
  }

  /**
   * Tests strict mode throws on empty string input.
   *
   * @covers ::process
   */
  public function testStrictModeEmptyStringThrows(): void {
    $processor = $this->createProcessor();
    $this->expectException(\RuntimeException::class);
    $processor->process(new ParameterBag([
      'json' => '',
      'parse_mode' => 'strict',
    ]));
  }

  /**
   * Tests strict mode throws on non-string, non-array input.
   *
   * @covers ::process
   */
  public function testStrictModeNonStringThrows(): void {
    $processor = $this->createProcessor();
    $this->expectException(\RuntimeException::class);
    $processor->process(new ParameterBag([
      'json' => 42,
      'parse_mode' => 'strict',
    ]));
  }

  /**
   * 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->assertSame($input, $result['data']);
  }

  /**
   * Tests strict mode emits only the data key (no success/parse_source).
   *
   * @covers ::process
   */
  public function testStrictModeEmitsOnlyData(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '{"x": 1}',
      'parse_mode' => 'strict',
    ]));

    $this->assertArrayHasKey('data', $result);
    $this->assertArrayNotHasKey('success', $result);
    $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['data']['default']);
    $this->assertArrayNotHasKey('success', $result);
    $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']);
    $this->assertNull($result['parse_source']);
  }

  /**
   * Tests tolerant mode accepts the valid JSON literal null.
   *
   * The data is NULL but success is TRUE — the flag disambiguates a parsed
   * null from a parse failure.
   *
   * @covers ::process
   */
  public function testTolerantModeAcceptsLiteralNull(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => 'null',
      'parse_mode' => 'tolerant',
    ]));

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

  /**
   * Tests tolerant mode accepts a valid JSON scalar.
   *
   * @covers ::process
   */
  public function testTolerantModeAcceptsScalar(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      'json' => '"hello"',
      'parse_mode' => 'tolerant',
    ]));

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

  /**
   * Tests tolerant mode never reports a parse_source when success is false.
   *
   * @covers ::process
   */
  public function testTolerantModeParseSourceNullOnFailure(): void {
    $processor = $this->createProcessor();

    foreach (['not json', "```json\nbad\n```", ''] as $input) {
      $result = $processor->process(new ParameterBag([
        'json' => $input,
        'parse_mode' => 'tolerant',
      ]));
      $this->assertFalse($result['success']);
      $this->assertNull($result['parse_source']);
    }
  }

  /**
   * 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);
  }

}
