<?php

declare(strict_types=1);

use Codeception\Configuration as CodeceptConfig;
use Codeception\Exception\ElementNotFound;
use Codeception\Exception\ModuleException;
use Codeception\Exception\TestRuntimeException;
use Codeception\Lib\Connector\Guzzle;
use Codeception\Lib\ModuleContainer;
use Codeception\Module\PhpBrowser;
use Codeception\Stub;

require_once dirname(dirname(dirname(__DIR__))) . '/data/app/data.php';
require_once __DIR__ . '/TestsForBrowsers.php';

use Codeception\Test\Cept;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack as GuzzleHandlerStack;
use GuzzleHttp\Middleware as GuzzleMiddleware;
use GuzzleHttp\Psr7\Response;
use Symfony\Component\BrowserKit\Cookie;

final class PhpBrowserTest extends TestsForBrowsers
{
    protected PhpBrowser $module;

    private array $history = [];

    protected function _setUp(): void
    {
        $container = Stub::make(ModuleContainer::class);
        $this->module = new PhpBrowser($container);
        $url = 'http://localhost:8000';
        $this->module->_setConfig(['url' => $url]);
        $this->module->_initialize();
        $this->module->_before($this->makeTest());
        $this->module->guzzle->getConfig('handler')->push(GuzzleMiddleware::history($this->history));
    }

    private function getLastRequest()
    {
        if (is_array($this->history)) {
            return end($this->history)['request'];
        }

        return $this->history->getLastRequest();
    }

    protected function _tearDown(): void
    {
        if ($this->module) {
            $this->module->_after($this->makeTest());
        }

        data::clean();
    }

    /**
     * @return \\Codeception\Test\Cept&\PHPUnit\Framework\MockObject\MockObject
     */
    private function makeTest()
    {
        return Stub::makeEmpty(Cept::class);
    }

    public function testAjax(): void
    {
        $this->module->amOnPage('/');
        $this->module->sendAjaxGetRequest('/info');
        $this->assertNotNull(data::get('ajax'));

        $this->module->sendAjaxPostRequest('/form/complex', ['show' => 'author']);
        $this->assertNotNull(data::get('ajax'));
        $post = data::get('form');
        $this->assertEquals('author', $post['show']);
    }

    public function testLinksWithNonLatin(): void
    {
        $this->module->amOnPage('/info');
        $this->module->seeLink('Ссылочка');
        $this->module->click('Ссылочка');
    }

    public function testHtmlSnapshot(): void
    {
        $this->module->amOnPage('/');
        $testName = "debugPhpBrowser";
        $this->module->makeHtmlSnapshot($testName);
        $this->assertFileExists(CodeceptConfig::outputDir() . 'debug/' . $testName . '.html');
        @unlink(CodeceptConfig::outputDir() . 'debug/' . $testName . '.html');
    }

    /**
     * @see https://github.com/Codeception/Codeception/issues/4509
     */
    public function testSeeTextAfterJSComparisionOperator(): void
    {
        $this->module->amOnPage('/info');
        $this->module->see('Text behind JS comparision');
    }

    public function testSetMultipleCookies(): void
    {
        $this->module->amOnPage('/');
        $cookie_name_1  = 'test_cookie';
        $cookie_value_1 = 'this is a test';
        $this->module->setCookie($cookie_name_1, $cookie_value_1);

        $cookie_name_2  = '2_test_cookie';
        $cookie_value_2 = '2 this is a test';
        $this->module->setCookie($cookie_name_2, $cookie_value_2);

        $this->module->seeCookie($cookie_name_1);
        $this->module->seeCookie($cookie_name_2);
        $this->module->dontSeeCookie('evil_cookie');

        $cookie1 = $this->module->grabCookie($cookie_name_1);
        $this->assertEquals($cookie_value_1, $cookie1);

        $cookie2 = $this->module->grabCookie($cookie_name_2);
        $this->assertEquals($cookie_value_2, $cookie2);

        $this->module->resetCookie($cookie_name_1);
        $this->module->dontSeeCookie($cookie_name_1);
        $this->module->seeCookie($cookie_name_2);
        $this->module->resetCookie($cookie_name_2);
        $this->module->dontSeeCookie($cookie_name_2);
    }

    public function testSessionsHaveIndependentCookies(): void
    {
        $this->module->amOnPage('/');
        $cookie_name_1  = 'test_cookie';
        $cookie_value_1 = 'this is a test';
        $this->module->setCookie($cookie_name_1, $cookie_value_1);

        $session = $this->module->_backupSession();
        $this->module->_initializeSession();

        $this->module->dontSeeCookie($cookie_name_1);

        $cookie_name_2  = '2_test_cookie';
        $cookie_value_2 = '2 this is a test';
        $this->module->setCookie($cookie_name_2, $cookie_value_2);

        $this->module->_loadSession($session);

        $this->module->dontSeeCookie($cookie_name_2);
        $this->module->seeCookie($cookie_name_1);
    }

    public function testSubmitFormGet(): void
    {
        $I = $this->module;
        $I->amOnPage('/search');
        $I->submitForm('form', ['searchQuery' => 'test']);
        $I->see('Success');
    }

    public function testHtmlRedirect(): void
    {
        $this->module->amOnPage('/redirect2');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/info');

        $this->module->amOnPage('/redirect_interval');
        $this->module->seeCurrentUrlEquals('/redirect_interval');
    }

    public function testHtmlRedirectWithParams(): void
    {
        $this->module->amOnPage('/redirect_params');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/search?one=1&two=2');
    }

    public function testMetaRefresh(): void
    {
        $this->module->amOnPage('/redirect_meta_refresh');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/info');
    }

    public function testMetaRefreshIsIgnoredIfIntervalIsLongerThanMaxInterval(): void
    {
        // prepare config
        $config = $this->module->_getConfig();
        $config['refresh_max_interval'] = 3; // less than 9
        $this->module->_reconfigure($config);
        $this->module->amOnPage('/redirect_meta_refresh');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/redirect_meta_refresh');
    }

    public function testRefreshRedirect(): void
    {
        $this->module->amOnPage('/redirect3');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/info');

        $this->module->amOnPage('/redirect_header_interval');
        $this->module->seeCurrentUrlEquals('/redirect_header_interval');
        $this->module->see('Welcome to test app!');
    }

    public function testRedirectWithGetParams(): void
    {
        $this->module->amOnPage('/redirect4');
        $this->module->seeInCurrentUrl('/search?ln=test@gmail.com&sn=testnumber');

        $params = data::get('params');
        $this->assertContains('test@gmail.com', $params);
    }

    public function testRedirectBaseUriHasPath(): void
    {
        // prepare config
        $config = $this->module->_getConfig();
        $config['url'] .= '/somepath'; // append path to the base url
        $this->module->_reconfigure($config);

        $this->module->amOnPage('/redirect_base_uri_has_path');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/somepath/info');
        $this->module->see('Lots of valuable data here');
    }

    public function testRedirectBaseUriHasPathAnd302Code(): void
    {
        // prepare config
        $config = $this->module->_getConfig();
        $config['url'] .= '/somepath'; // append path to the base url
        $this->module->_reconfigure($config);

        $this->module->amOnPage('/redirect_base_uri_has_path_302');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/somepath/info');
        $this->module->see('Lots of valuable data here');
    }

    public function testRelativeRedirect(): void
    {
        // test relative redirects where the effective request URI is in a
        // subdirectory
        $this->module->amOnPage('/relative/redirect');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/relative/info');

        // also, test relative redirects where the effective request URI is not
        // in a subdirectory
        $this->module->amOnPage('/relative_redirect');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/info');
    }

    public function testChainedRedirects(): void
    {
        $this->module->amOnPage('/redirect_twice');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/info');
    }

    public function testDisabledRedirects(): void
    {
        $this->module->client->followRedirects(false);
        $this->module->amOnPage('/redirect_twice');
        $this->module->seeResponseCodeIs(302);
        $this->module->seeCurrentUrlEquals('/redirect_twice');
    }

    public function testRedirectLimitReached(): void
    {
        $this->module->client->setMaxRedirects(1);
        try {
            $this->module->amOnPage('/redirect_twice');
            $this->fail('redirect limit is not respected');
        } catch (LogicException $logicException) {
            $this->assertEquals(
                'The maximum number (1) of redirections was reached.',
                $logicException->getMessage(),
                'redirect limit is respected'
            );
        }
    }

    public function testRedirectLimitNotReached(): void
    {
        $this->module->client->setMaxRedirects(2);
        $this->module->amOnPage('/redirect_twice');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCurrentUrlEquals('/info');
    }

    public function testLocationHeaderDoesNotRedirectWhenStatusCodeIs201(): void
    {
        $this->module->amOnPage('/location_201');
        $this->module->seeResponseCodeIs(201);
        $this->module->seeCurrentUrlEquals('/location_201');
    }

    public function testRedirectToAnotherDomainUsingSchemalessUrl(): void
    {

        $this->module->_reconfigure([
            'handler' => new MockHandler([
                new Response(302, ['Location' => '//example.org/']),
                new Response(200, [], 'Cool stuff')
            ])
        ]);
        $this->module->amOnUrl('http://fictional.redirector/redirect-to?url=//example.org/');

        $currentUrl = $this->module->client->getHistory()->current()->getUri();
        $this->assertSame('http://example.org/', $currentUrl);
    }

    public function testSetCookieByHeader(): void
    {
        $this->module->amOnPage('/cookies2');
        $this->module->seeResponseCodeIs(200);
        $this->module->seeCookie('a');
        $this->assertEquals('b', $this->module->grabCookie('a'));
        $this->module->seeCookie('c');
    }

    public function testSettingContentTypeFromHtml(): void
    {
        $this->module->amOnPage('/content-iso');
        $charset = $this->module->client->getResponse()->getHeader('Content-Type');
        $this->assertEquals('text/html;charset=ISO-8859-1', $charset);
    }

    public function testSettingCharsetFromHtml(): void
    {
        $this->module->amOnPage('/content-cp1251');
        $charset = $this->module->client->getResponse()->getHeader('Content-Type');
        $this->assertEquals('text/html;charset=windows-1251', $charset);
    }

    /**
     * @Issue https://github.com/Codeception/Codeception/issues/933
     */
    public function testSubmitFormWithQueries(): void
    {
        $this->module->amOnPage('/form/example3');
        $this->module->seeElement('form');
        $this->module->submitForm('form', [
                'name' => 'jon',
        ]);
        $form = data::get('form');
        $this->assertEquals('jon', $form['name']);
        $this->module->seeCurrentUrlEquals('/form/example3?validate=yes');
    }

    public function testHeadersBySetHeader(): void
    {
        $this->module->setHeader('xxx', 'yyyy');
        $this->module->amOnPage('/');
        $this->assertTrue($this->getLastRequest()->hasHeader('xxx'));
    }

    public function testDeleteHeaders(): void
    {
        $this->module->setHeader('xxx', 'yyyy');
        $this->module->deleteHeader('xxx');
        $this->module->amOnPage('/');
        $this->assertFalse($this->getLastRequest()->hasHeader('xxx'));
    }

    public function testDeleteHeadersByEmptyValue(): void
    {
        $this->module->setHeader('xxx', 'yyyy');
        $this->module->setHeader('xxx', '');
        $this->module->amOnPage('/');
        $this->assertFalse($this->getLastRequest()->hasHeader('xxx'));
    }

    public function testCurlOptions(): void
    {
        $this->module->_setConfig(['url' => 'http://google.com', 'curl' => ['CURLOPT_NOBODY' => true]]);
        $this->module->_initialize();
        if (method_exists($this->module->guzzle, 'getConfig')) {
            $config = $this->module->guzzle->getConfig();
        } else {
            $config = $this->module->guzzle->getDefaultOption('config');
        }

        $this->assertArrayHasKey('curl', $config);
        $this->assertArrayHasKey(CURLOPT_NOBODY, $config['curl']);
    }


    public function testCurlSslOptions(): void
    {
        $this->module->_setConfig([
            'url' => 'https://github.com',
            'curl' => [
                'CURLOPT_NOBODY' => true,
                'CURLOPT_SSL_CIPHER_LIST' => 'TLSv1',
            ]]);
        $this->module->_initialize();

        $config = $this->module->guzzle->getConfig();

        $this->assertArrayHasKey('curl', $config);
        $this->assertArrayHasKey(CURLOPT_SSL_CIPHER_LIST, $config['curl']);
        $this->module->amOnPage('/');
        $this->assertSame('', $this->module->_getResponseContent(), 'CURLOPT_NOBODY setting is not respected');
    }

    public function testHttpAuth(): void
    {
        $this->module->amOnPage('/auth');
        $this->module->seeResponseCodeIs(401);
        $this->module->see('Unauthorized');
        $this->module->amHttpAuthenticated('davert', 'password');
        $this->module->amOnPage('/auth');
        $this->module->seeResponseCodeIs(200);
        $this->module->dontSee('Unauthorized');
        $this->module->see("Welcome, davert");
        $this->module->amHttpAuthenticated('', '');
        $this->module->amOnPage('/auth');
        $this->module->seeResponseCodeIs(401);
        $this->module->amHttpAuthenticated('davert', '123456');
        $this->module->amOnPage('/auth');
        $this->module->see('Forbidden');
    }

    public function testRawGuzzle(): void
    {
        $code = $this->module->executeInGuzzle(function (GuzzleClient $guzzleClient): int {
            $response = $guzzleClient->get('/info');
            return $response->getStatusCode();
        });
        $this->assertEquals(200, $code);
    }

    /**
     * If we have a form with fields like
     * ```
     * <input type="file" name="foo" />
     * <input type="file" name="foo[bar]" />
     * ```
     * then only array variable will be used while simple variable will be ignored in php $_FILES
     * (eg $_FILES = [
     *                 foo => [
     *                     tmp_name => [
     *                         'bar' => 'asdf'
     *                     ],
     *                     //...
     *                ]
     *              ]
     * )
     * (notice there is no entry for file "foo", only for file "foo[bar]"
     * this will check if current element contains inner arrays within it's keys
     * so we can ignore element itself and only process inner files
     */
    public function testFormWithFilesInOnlyArray(): void
    {
        $this->shouldFail();
        $this->module->amOnPage('/form/example13');
        $this->module->attachFile('foo', 'app/avatar.jpg');
        $this->module->attachFile('foo[bar]', 'app/avatar.jpg');
        $this->module->click('Submit');
    }

    public function testDoubleSlash(): void
    {
        $I = $this->module;
        $I->amOnPage('/register');
        $I->submitForm('form', ['test' => 'test']);

        $formUrl = $this->module->client->getHistory()->current()->getUri();
        $formPath = parse_url($formUrl)['path'];
        $this->assertEquals($formPath, '/register');
    }

    public function testFillFieldWithoutPage(): void
    {
        $this->expectException(ModuleException::class);
        $this->module->fillField('#name', 'Nothing special');
    }

    public function testArrayFieldSubmitForm(): void
    {
        $this->module->amOnPage('/form/example17');
        $this->module->submitForm(
            'form',
            [
                'FooBar' => ['bar' => 'booze'],
                'Food' => [
                    'beer' => [
                        'yum' => ['yeah' => 'crunked']
                    ]
                ]
            ]
        );
        $data = data::get('form');
        $this->assertEquals('booze', $data['FooBar']['bar']);
        $this->assertEquals('crunked', $data['Food']['beer']['yum']['yeah']);
    }

    public function testCookiesForDomain(): void
    {
        $mockHandler = new MockHandler([
            new Response(200, ['X-Foo' => 'Bar']),
        ]);
        $guzzleHandlerStack = GuzzleHandlerStack::create($mockHandler);
        $guzzleHandlerStack->push(GuzzleMiddleware::history($this->history));

        $client = new GuzzleClient(['handler' => $guzzleHandlerStack, 'base_uri' => 'https://codeception.com']);
        $guzzle = new Guzzle();
        $guzzle->setClient($client);
        $guzzle->getCookieJar()->set(new Cookie('hello', 'world'));
        $guzzle->request('GET', 'https://codeception.com/');
        $this->assertArrayHasKey('cookies', $this->history[0]['options']);
        /** @var $cookie GuzzleHttp\Cookie\SetCookie  **/
        $cookies = $this->history[0]['options']['cookies']->toArray();
        $cookie = reset($cookies);
        $this->assertEquals('codeception.com', $cookie['Domain']);
    }

    /**
     * @issue https://github.com/Codeception/Codeception/issues/2653
     */
    public function testSetCookiesByOptions(): void
    {
        $config = $this->module->_getConfig();
        $config['cookies'] = [
            [
                'Name' => 'foo',
                'Value' => 'bar1',
            ],
            [
                'Name' => 'baz',
                'Value' => 'bar2',
            ],
        ];
        $this->module->_reconfigure($config);
        // this url redirects if cookies are present
        $this->module->amOnPage('/cookies');
        $this->module->seeCurrentUrlEquals('/info');
    }

    /**
     * @issue https://github.com/Codeception/Codeception/issues/2234
     */
    public function testEmptyValueOfCookie(): void
    {
        //set cookie
        $this->module->amOnPage('/cookies2');

        $this->module->amOnPage('/unset-cookie');
        $this->module->seeResponseCodeIs(200);
        $this->module->dontSeeCookie('a');
    }

    public function testRequestApi(): void
    {
        $this->expectException(ModuleException::class);
        $response = $this->module->_request('POST', '/form/try', ['user' => 'davert']);
        $data = data::get('form');
        $this->assertEquals('davert', $data['user']);
        $this->assertIsString($response);
        $this->assertStringContainsString('Welcome to test app', $response);
        $this->module->click('Welcome to test app'); // page not loaded
    }

    public function testLoadPageApi(): void
    {
        $this->module->_loadPage('POST', '/form/try', ['user' => 'davert']);
        $data = data::get('form');
        $this->assertEquals('davert', $data['user']);
        $this->module->see('Welcome to test app');
        $this->module->click('More info');
        $this->module->seeInCurrentUrl('/info');
    }

    /**
     * @issue https://github.com/Codeception/Codeception/issues/2408
     */
    public function testClickFailure(): void
    {
        $this->module->amOnPage('/info');
        $this->expectException(ElementNotFound::class);
        $this->expectExceptionMessage("'Sign In!' is invalid CSS and XPath selector and Link or Button element with 'name=Sign In!' was not found");
        $this->module->click('Sign In!');
    }

    /**
     * @issue https://github.com/Codeception/Codeception/issues/2841
     */
    public function testSubmitFormDoesNotKeepGetParameters(): void
    {
        $this->module->amOnPage('/form/bug2841?stuff=other');
        $this->module->fillField('#texty', 'thingshjere');
        $this->module->click('#submit-registration');
        $this->assertEmpty(data::get('query'), 'Query string is not empty');
    }

    public function testClickLinkAndFillField(): void
    {
        $this->module->amOnPage('/info');
        $this->module->click('Sign in!');
        $this->module->seeCurrentUrlEquals('/login');
        $this->module->fillField('email', 'email@example.org');
    }

    public function testClickSelectsClickableElementFromMatches(): void
    {
        $this->module->amOnPage('/form/multiple_matches');
        $this->module->click('Press Me!');
        $this->module->seeCurrentUrlEquals('/info');
    }

    public function testClickSelectsClickableElementFromMatchesUsingCssLocator(): void
    {
        $this->module->amOnPage('/form/multiple_matches');
        $this->module->click(['css' => '.link']);
        $this->module->seeCurrentUrlEquals('/info');
    }

    public function testClickingOnButtonOutsideFormDoesNotCauseFatalError(): void
    {
        $this->expectException(TestRuntimeException::class);
        $this->expectExceptionMessage('Button is not inside a link or a form');
        $this->module->amOnPage('/form/button-not-in-form');
        $this->module->click('Submit 2');
    }

    public function testSubmitFormWithoutEmptyOptionsInSelect(): void
    {
        $this->module->amOnPage('/form/bug3824');
        $this->module->submitForm('form', []);
        $this->module->dontSee('ERROR');
    }

    /**
     * @issue https://github.com/Codeception/Codeception/issues/3953
     */
    public function testFillFieldInGetFormWithoutId(): void
    {
        $this->module->amOnPage('/form/bug3953');
        $this->module->selectOption('select_name', 'two');
        $this->module->fillField('search_name', 'searchterm');
        $this->module->click('Submit');

        $params = data::get('query');
        $this->assertEquals('two', $params['select_name']);
        $this->assertEquals('searchterm', $params['search_name']);
    }

    public function testGrabPageSourceWhenNotOnPage(): void
    {
        $this->expectException(ModuleException::class);
        $this->expectExceptionMessage('Page not loaded. Use `$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it');
        $this->module->grabPageSource();
    }

    public function testGrabPageSourceWhenOnPage(): void
    {
        $this->module->amOnPage('/minimal');
        $sourceExpected =
        <<<HTML
<!DOCTYPE html>
<html>
    <head>
        <title>
            Minimal page
        </title>
    </head>
    <body>
        <h1>
            Minimal page
        </h1>
    </body>
</html>

HTML
        ;
        $sourceActual = $this->module->grabPageSource();
        $this->assertXmlStringEqualsXmlString($sourceExpected, $sourceActual);
    }

    /**
     * @issue https://github.com/Codeception/Codeception/issues/4383
     */
    public function testSecondAmOnUrlWithEmptyPath(): void
    {
        $this->module->amOnUrl('http://localhost:8000/info');
        $this->module->see('Lots of valuable data here');
        $this->module->amOnUrl('http://localhost:8000');
        $this->module->dontSee('Lots of valuable data here');
    }

    public function testSetUserAgentUsingConfig(): void
    {
        $this->module->_setConfig(['headers' => ['User-Agent' => 'Codeception User Agent Test 1.0']]);
        $this->module->_initialize();

        $this->module->amOnPage('/user-agent');

        $response = $this->module->grabPageSource();
        $this->assertEquals('Codeception User Agent Test 1.0', $response, 'Incorrect user agent');
    }

    public function testIfStatusCodeIsWithin2xxRange(): void
    {
        $this->module->amOnPage('http://localhost:8000/status.php?status=200');
        $this->module->seeResponseCodeIsSuccessful();

        $this->module->amOnPage('http://localhost:8000/status.php?status=299');
        $this->module->seeResponseCodeIsSuccessful();
    }

    public function testIfStatusCodeIsWithin3xxRange(): void
    {
        $this->module->amOnPage('http://localhost:8000/status.php?status=300');
        $this->module->seeResponseCodeIsRedirection();

        $this->module->amOnPage('http://localhost:8000/status.php?status=399');
        $this->module->seeResponseCodeIsRedirection();
    }

    public function testIfStatusCodeIsWithin4xxRange(): void
    {
        $this->module->amOnPage('http://localhost:8000/status.php?status=400');
        $this->module->seeResponseCodeIsClientError();

        $this->module->amOnPage('http://localhost:8000/status.php?status=499');
        $this->module->seeResponseCodeIsClientError();
    }

    public function testIfStatusCodeIsWithin5xxRange(): void
    {
        $this->module->amOnPage('http://localhost:8000/status.php?status=500');
        $this->module->seeResponseCodeIsServerError();

        $this->module->amOnPage('http://localhost:8000/status.php?status=599');
        $this->module->seeResponseCodeIsServerError();
    }

    /**
     * @issue https://github.com/Codeception/Codeception/issues/5547
     */
    public function testSelectOptionByTextWhenItHasNoValue(): void
    {
        $this->module->amOnPage('/form/bug5547');
        $this->module->selectOption('#_payment_type', 'qwerty');
        $this->module->click('Submit');

        $form = data::get('form');
        $this->assertEquals('qwerty', $form['payment_type']);
    }
}
