<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop\Unit\Exception;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop\Exception\EntityProcessingException;
use Drupal\flowdrop\Exception\FlowDropException;
use Drupal\flowdrop\Exception\InvalidNodeConfigurationException;
use Drupal\flowdrop\Exception\NodeProcessingException;
use Drupal\flowdrop\Exception\RetryableExceptionInterface;
use Drupal\flowdrop\Exception\RetryableNodeProcessingException;

/**
 * Tests the exception hierarchy is stable.
 *
 * @group flowdrop
 */
class ExceptionHierarchyTest extends UnitTestCase {

  /**
   * Tests FlowDropException extends RuntimeException.
   *
   * @covers \Drupal\flowdrop\Exception\FlowDropException
   */
  public function testFlowDropExceptionExtendsRuntimeException(): void {
    $exception = new FlowDropException("test");
    $this->assertInstanceOf(\RuntimeException::class, $exception);
    $this->assertEquals("test", $exception->getMessage());
  }

  /**
   * Tests NodeProcessingException extends FlowDropException.
   *
   * @covers \Drupal\flowdrop\Exception\NodeProcessingException
   */
  public function testNodeProcessingExceptionHierarchy(): void {
    $exception = new NodeProcessingException("node error", 100);
    $this->assertInstanceOf(FlowDropException::class, $exception);
    $this->assertInstanceOf(\RuntimeException::class, $exception);
    $this->assertEquals(100, $exception->getCode());
  }

  /**
   * Tests EntityProcessingException extends FlowDropException.
   *
   * @covers \Drupal\flowdrop\Exception\EntityProcessingException
   */
  public function testEntityProcessingExceptionHierarchy(): void {
    $exception = new EntityProcessingException("entity error");
    $this->assertInstanceOf(FlowDropException::class, $exception);
  }

  /**
   * Tests InvalidNodeConfigurationException extends FlowDropException.
   *
   * @covers \Drupal\flowdrop\Exception\InvalidNodeConfigurationException
   */
  public function testInvalidNodeConfigurationExceptionHierarchy(): void {
    $previous = new \Exception("root cause");
    $exception = new InvalidNodeConfigurationException("config error", 0, $previous);
    $this->assertInstanceOf(FlowDropException::class, $exception);
    $this->assertSame($previous, $exception->getPrevious());
  }

  /**
   * Tests the retryable exception sits under NodeProcessingException + marker.
   *
   * It must remain catchable by the orchestrator's generic \Exception /
   * \RuntimeException handler while carrying the marker that flips that
   * handler from fail to retry.
   *
   * @covers \Drupal\flowdrop\Exception\RetryableNodeProcessingException
   */
  public function testRetryableNodeProcessingExceptionHierarchy(): void {
    $exception = new RetryableNodeProcessingException("transient", 503);
    $this->assertInstanceOf(NodeProcessingException::class, $exception);
    $this->assertInstanceOf(FlowDropException::class, $exception);
    $this->assertInstanceOf(\RuntimeException::class, $exception);
    $this->assertInstanceOf(RetryableExceptionInterface::class, $exception);
    $this->assertEquals(503, $exception->getCode());
  }

  /**
   * Tests retryability is opt-in: a plain node failure is NOT retryable.
   *
   * This is the discriminator the orchestrator relies on — everything that
   * does not implement the marker must be treated as a terminal failure.
   *
   * @covers \Drupal\flowdrop\Exception\NodeProcessingException
   */
  public function testNonRetryableExceptionsAreNotMarked(): void {
    $this->assertNotInstanceOf(
      RetryableExceptionInterface::class,
      new NodeProcessingException("permanent"),
    );
    $this->assertNotInstanceOf(
      RetryableExceptionInterface::class,
      new FlowDropException("generic"),
    );
  }

}
