<?php

declare(strict_types=1);

namespace Tests\Framework\LiveComponents;

use App\Framework\DateTime\SystemClock;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\Exceptions\StateValidationException;
use App\Framework\LiveComponents\Exceptions\UnauthorizedActionException;
use App\Framework\LiveComponents\LiveComponentHandler;
use App\Framework\LiveComponents\Security\SessionBasedAuthorizationChecker;
use App\Framework\LiveComponents\Validation\DefaultStateValidator;
use App\Framework\LiveComponents\Validation\SchemaCache;
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
use App\Framework\LiveComponents\ValueObjects\ComponentUpdate;
use App\Framework\Random\SecureRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;

/**
 * Test harness for LiveComponent testing
 *
 * Provides comprehensive helper methods for testing LiveComponents:
 * - Component setup and lifecycle
 * - Action execution with CSRF, Authorization, Validation
 * - State assertions
 * - Event assertions
 * - User/Permission mocking
 *
 * Usage in Pest tests:
 * ```php
 * use Tests\Framework\LiveComponents\ComponentTestCase;
 *
 * uses(ComponentTestCase::class);
 *
 * it('executes action', function () {
 *     $component = $this->createComponent(CounterComponent::class);
 *     $result = $this->callAction($component, 'increment');
 *     $this->assertStateEquals($result, ['count' => 1]);
 * });
 * ```
 */
trait ComponentTestCase
{
    protected Session $session;

    protected LiveComponentHandler $handler;

    protected ComponentEventDispatcher $eventDispatcher;

    protected string $csrfToken;

    /**
     * Setup test environment
     *
     * Creates session, handler, and generates CSRF token.
     * Call this in beforeEach() hook.
     */
    protected function setUpComponentTest(): void
    {
        // Create session
        $sessionId = SessionId::fromString(bin2hex(random_bytes(16)));
        $clock = new SystemClock();
        $randomGenerator = new SecureRandomGenerator();
        $csrfGenerator = new CsrfTokenGenerator($randomGenerator);

        $this->session = Session::fromArray($sessionId, $clock, $csrfGenerator, []);

        // Create handler dependencies
        $this->eventDispatcher = new ComponentEventDispatcher();
        $authChecker = new SessionBasedAuthorizationChecker($this->session);
        $stateValidator = new DefaultStateValidator();
        $schemaCache = new SchemaCache();

        $this->handler = new LiveComponentHandler(
            $this->eventDispatcher,
            $this->session,
            $authChecker,
            $stateValidator,
            $schemaCache
        );
    }

    /**
     * Authenticate user with permissions
     *
     * @param array<string> $permissions User permissions
     * @param int $userId User ID
     */
    protected function actingAs(array $permissions = [], int $userId = 1): self
    {
        $this->session->set('user', [
            'id' => $userId,
            'permissions' => $permissions,
        ]);

        return $this;
    }

    /**
     * Call component action
     *
     * Automatically generates CSRF token for the component.
     *
     * @param LiveComponentContract $component Component instance
     * @param string $method Action method name
     * @param array<string, mixed> $params Action parameters
     * @return ComponentUpdate Action result
     */
    protected function callAction(
        LiveComponentContract $component,
        string $method,
        array $params = []
    ): ComponentUpdate {
        // Generate CSRF token for component
        $formId = 'livecomponent:' . $component->getId()->toString();
        $csrfToken = $this->session->csrf->generateToken($formId);

        $actionParams = ActionParameters::fromArray($params, $csrfToken);

        return $this->handler->handle($component, $method, $actionParams);
    }

    /**
     * Assert action executes successfully
     *
     * @param LiveComponentContract $component Component instance
     * @param string $method Action method name
     * @param array<string, mixed> $params Action parameters
     */
    protected function assertActionExecutes(
        LiveComponentContract $component,
        string $method,
        array $params = []
    ): ComponentUpdate {
        $result = $this->callAction($component, $method, $params);

        expect($result)->toBeInstanceOf(ComponentUpdate::class);

        return $result;
    }

    /**
     * Assert action throws exception
     *
     * @param LiveComponentContract $component Component instance
     * @param string $method Action method name
     * @param string $exceptionClass Expected exception class
     * @param array<string, mixed> $params Action parameters
     */
    protected function assertActionThrows(
        LiveComponentContract $component,
        string $method,
        string $exceptionClass,
        array $params = []
    ): void {
        $thrown = false;
        $caughtException = null;

        try {
            $this->callAction($component, $method, $params);
        } catch (\Throwable $e) {
            $thrown = true;
            $caughtException = $e;
        }

        if (! $thrown) {
            throw new \AssertionError("Expected exception {$exceptionClass} to be thrown, but no exception was thrown");
        }

        if (! ($caughtException instanceof $exceptionClass)) {
            throw new \AssertionError(
                "Expected exception of type {$exceptionClass}, got " . get_class($caughtException)
            );
        }
    }

    /**
     * Assert action requires authentication
     *
     * @param LiveComponentContract $component Component with protected action
     * @param string $method Protected action method
     */
    protected function assertActionRequiresAuth(
        LiveComponentContract $component,
        string $method
    ): void {
        $this->assertActionThrows(
            $component,
            $method,
            UnauthorizedActionException::class
        );
    }

    /**
     * Assert action requires permission
     *
     * @param LiveComponentContract $component Component with protected action
     * @param string $method Protected action method
     * @param array<string> $withoutPermissions User permissions (should fail)
     */
    protected function assertActionRequiresPermission(
        LiveComponentContract $component,
        string $method,
        array $withoutPermissions = []
    ): void {
        $this->actingAs($withoutPermissions);

        $this->assertActionThrows(
            $component,
            $method,
            UnauthorizedActionException::class
        );
    }

    /**
     * Assert state equals expected values
     *
     * @param ComponentUpdate $result Action result
     * @param array<string, mixed> $expected Expected state data
     */
    protected function assertStateEquals(ComponentUpdate $result, array $expected): void
    {
        $actualState = $result->state->data;

        foreach ($expected as $key => $value) {
            expect($actualState)->toHaveKey($key);
            expect($actualState[$key])->toBe($value);
        }
    }

    /**
     * Assert state has key
     *
     * @param ComponentUpdate $result Action result
     * @param string $key State key
     */
    protected function assertStateHas(ComponentUpdate $result, string $key): void
    {
        expect($result->state->data)->toHaveKey($key);
    }

    /**
     * Assert state validates against schema
     *
     * @param ComponentUpdate $result Action result
     */
    protected function assertStateValidates(ComponentUpdate $result): void
    {
        // If we got here without StateValidationException, validation passed
        expect($result)->toBeInstanceOf(ComponentUpdate::class);
    }

    /**
     * Assert event was dispatched
     *
     * @param ComponentUpdate $result Action result
     * @param string $eventName Event name
     */
    protected function assertEventDispatched(ComponentUpdate $result, string $eventName): void
    {
        $events = $result->events;
        $found = false;

        foreach ($events as $event) {
            if ($event->name === $eventName) {
                $found = true;

                break;
            }
        }

        expect($found)->toBeTrue("Event '{$eventName}' was not dispatched");
    }

    /**
     * Assert no events were dispatched
     *
     * @param ComponentUpdate $result Action result
     */
    protected function assertNoEventsDispatched(ComponentUpdate $result): void
    {
        expect($result->events)->toBeEmpty();
    }

    /**
     * Assert event count
     *
     * @param ComponentUpdate $result Action result
     * @param int $count Expected event count
     */
    protected function assertEventCount(ComponentUpdate $result, int $count): void
    {
        expect($result->events)->toHaveCount($count);
    }

    /**
     * Get state value from result
     *
     * @param ComponentUpdate $result Action result
     * @param string $key State key
     * @return mixed State value
     */
    protected function getStateValue(ComponentUpdate $result, string $key): mixed
    {
        return $result->state->data[$key] ?? null;
    }
}
