<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Bundle\FrameworkBundle\Test;

use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\Constraint\LogicalAnd;
use PHPUnit\Framework\Constraint\LogicalNot;
use PHPUnit\Framework\ExpectationFailedException;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\History;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
use Symfony\Component\BrowserKit\Response as BrowserKitResponse;
use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Test\Constraint as ResponseConstraint;

/**
 * Ideas borrowed from Laravel Dusk's assertions.
 *
 * @see https://laravel.com/docs/5.7/dusk#available-assertions
 */
trait BrowserKitAssertionsTrait
{
    private static bool $defaultVerboseMode = true;

    public static function setBrowserKitAssertionsAsVerbose(bool $verbose): void
    {
        self::$defaultVerboseMode = $verbose;
    }

    public static function assertResponseIsSuccessful(string $message = '', ?bool $verbose = null): void
    {
        self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful($verbose ?? self::$defaultVerboseMode), $message);
    }

    public static function assertResponseStatusCodeSame(int $expectedCode, string $message = '', ?bool $verbose = null): void
    {
        self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode, $verbose ?? self::$defaultVerboseMode), $message);
    }

    public static function assertResponseFormatSame(?string $expectedFormat, string $message = '', ?bool $verbose = null): void
    {
        self::assertThatForResponse(new ResponseConstraint\ResponseFormatSame(self::getRequest(), $expectedFormat, $verbose ?? self::$defaultVerboseMode), $message);
    }

    public static function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', ?bool $verbose = null): void
    {
        $verbose ??= self::$defaultVerboseMode;

        $constraints = [new ResponseConstraint\ResponseIsRedirected($verbose)];
        if ($expectedLocation) {
            if (class_exists(ResponseConstraint\ResponseHeaderLocationSame::class)) {
                $constraints[] = new ResponseConstraint\ResponseHeaderLocationSame(self::getRequest(), $expectedLocation);
            } else {
                $constraints[] = new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation);
            }
        }
        if ($expectedCode) {
            $constraints[] = new ResponseConstraint\ResponseStatusCodeSame($expectedCode, $verbose);
        }

        // LogicalAnd describes its failure by exporting the whole response, body included, so it
        // cannot be used when $verbose asked for the body to be omitted; assert one by one instead
        if ($verbose && 1 < \count($constraints)) {
            $constraints = [LogicalAnd::fromConstraints(...$constraints)];
        }

        foreach ($constraints as $constraint) {
            self::assertThatForResponse($constraint, $message);
        }
    }

    public static function assertResponseHasHeader(string $headerName, string $message = ''): void
    {
        self::assertThatForResponse(new ResponseConstraint\ResponseHasHeader($headerName), $message);
    }

    public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void
    {
        self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message);
    }

    public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void
    {
        self::assertThatForResponse(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message);
    }

    public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
    {
        self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message);
    }

    public static function assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
    {
        self::assertThatForResponse(new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message);
    }

    public static function assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
    {
        self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message);
    }

    public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = ''): void
    {
        self::assertThatForResponse(LogicalAnd::fromConstraints(
            new ResponseConstraint\ResponseHasCookie($name, $path, $domain),
            new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain)
        ), $message);
    }

    public static function assertResponseIsUnprocessable(string $message = '', ?bool $verbose = null): void
    {
        self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable($verbose ?? self::$defaultVerboseMode), $message);
    }

    public static function assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
    {
        self::assertThatForClient(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message);
    }

    public static function assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
    {
        self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message);
    }

    public static function assertBrowserHistoryIsOnFirstPage(string $message = ''): void
    {
        if (!method_exists(History::class, 'isFirstPage')) {
            throw new \LogicException('The `assertBrowserHistoryIsOnFirstPage` method requires symfony/browser-kit >= 7.4.');
        }
        self::assertThatForClient(new BrowserKitConstraint\BrowserHistoryIsOnFirstPage(), $message);
    }

    public static function assertBrowserHistoryIsNotOnFirstPage(string $message = ''): void
    {
        if (!method_exists(History::class, 'isFirstPage')) {
            throw new \LogicException('The `assertBrowserHistoryIsNotOnFirstPage` method requires symfony/browser-kit >= 7.4.');
        }
        self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHistoryIsOnFirstPage()), $message);
    }

    public static function assertBrowserHistoryIsOnLastPage(string $message = ''): void
    {
        if (!method_exists(History::class, 'isLastPage')) {
            throw new \LogicException('The `assertBrowserHistoryIsOnLastPage` method requires symfony/browser-kit >= 7.4.');
        }
        self::assertThatForClient(new BrowserKitConstraint\BrowserHistoryIsOnLastPage(), $message);
    }

    public static function assertBrowserHistoryIsNotOnLastPage(string $message = ''): void
    {
        if (!method_exists(History::class, 'isLastPage')) {
            throw new \LogicException('The `assertBrowserHistoryIsNotOnLastPage` method requires symfony/browser-kit >= 7.4.');
        }
        self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHistoryIsOnLastPage()), $message);
    }

    public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', ?string $domain = null, string $message = ''): void
    {
        self::assertThatForClient(LogicalAnd::fromConstraints(
            new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain),
            new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain)
        ), $message);
    }

    public static function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void
    {
        self::assertThat(self::getRequest(), new ResponseConstraint\RequestAttributeValueSame($name, $expectedValue), $message);
    }

    public static function assertRouteSame(string $expectedRoute, array $parameters = [], string $message = ''): void
    {
        $constraint = new ResponseConstraint\RequestAttributeValueSame('_route', $expectedRoute);
        $constraints = [];
        foreach ($parameters as $key => $value) {
            $constraints[] = new ResponseConstraint\RequestAttributeValueSame($key, $value);
        }
        if ($constraints) {
            $constraint = LogicalAnd::fromConstraints($constraint, ...$constraints);
        }

        self::assertThat(self::getRequest(), $constraint, $message);
    }

    public static function assertThatForResponse(Constraint $constraint, string $message = ''): void
    {
        try {
            self::assertThat(self::getResponse(), $constraint, $message);
        } catch (ExpectationFailedException $exception) {
            if (($serverExceptionMessage = self::getResponse()->headers->get('X-Debug-Exception'))
                && ($serverExceptionFile = self::getResponse()->headers->get('X-Debug-Exception-File'))) {
                $serverExceptionFile = explode(':', $serverExceptionFile);
                $exception->__construct($exception->getMessage(), $exception->getComparisonFailure(), new \ErrorException(rawurldecode($serverExceptionMessage), 0, 1, rawurldecode($serverExceptionFile[0]), $serverExceptionFile[1]), $exception->getPrevious());
            }

            throw $exception;
        }
    }

    public static function assertThatForClient(Constraint $constraint, string $message = ''): void
    {
        self::assertThat(self::getClient(), $constraint, $message);
    }

    protected static function getClient(?AbstractBrowser $newClient = null): ?AbstractBrowser
    {
        static $client;

        if (0 < \func_num_args()) {
            return $client = $newClient;
        }

        if (!$client instanceof AbstractBrowser) {
            static::fail(\sprintf('A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', __CLASS__));
        }

        return $client;
    }

    private static function getResponse(): Response
    {
        if (!$response = self::getClient()->getResponse()) {
            static::fail('A client must have an HTTP Response to make assertions. Did you forget to make an HTTP request?');
        }

        if ($response instanceof BrowserKitResponse) {
            return new Response(
                $response->getContent(),
                $response->getStatusCode(),
                $response->getHeaders()
            );
        }

        return $response;
    }

    private static function getRequest(): Request
    {
        if (!$request = self::getClient()->getRequest()) {
            static::fail('A client must have an HTTP Request to make assertions. Did you forget to make an HTTP request?');
        }

        if ($request instanceof BrowserKitRequest) {
            return Request::create(
                $request->getUri(),
                $request->getMethod(),
                $request->getParameters(),
                $request->getCookies(),
                $request->getFiles(),
                $request->getServer(),
                $request->getContent()
            );
        }

        return $request;
    }
}
