<?php

namespace Autogedal\TbiBank\Test\Unit;

use Autogedal\TbiBank\Model\CspCollector;
use Autogedal\TbiBank\Model\Environment;
use Autogedal\TbiBank\Plugin\ApiEndpoint;
use Autogedal\TbiBank\Plugin\FrontendUrls;
use Autogedal\TbiBank\Plugin\HelperConfig;
use Autogedal\TbiBank\Plugin\JsonUrl;
use Autogedal\TbiBank\Plugin\SettingsSync;
use Magento\Config\Model\ResourceModel\Config;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\HTTP\Client\Curl;
use PHPUnit\Framework\TestCase;
use TbiBank\FusionPay\Helper\Data;
use TbiBank\FusionPay\Observer\AfterConfigSave;

class EnvironmentIntegrationTest extends TestCase
{
    private array $values;
    private Environment $environment;

    protected function setUp(): void
    {
        $this->values = [
            Environment::CONNECTION . 'test_mode' => '1',
            Environment::CONNECTION . 'production_url' => Environment::VENDOR_ORIGIN,
            Environment::CONNECTION . 'uat_url' => Environment::UAT_ORIGIN,
            'tbi_settings/uat_settings/reseller_code' => 'test-code',
            'tbi_settings/uat_settings/reseller_key' => 'test-reseller',
            'tbi_settings/uat_settings/encryption_key' => '0:3:ZW5jcnlwdGVkLXRlc3Qta2V5',
            'tbi_settings/uat_settings/status' => 'Inactive',
            'tbi_settings/uat_label_settings/maximum_visible' => '2000',
        ];
        $config = $this->createMock(ScopeConfigInterface::class);
        $config->method('getValue')->willReturnCallback(function ($path, $scope = 'default', $scopeCode = null) {
            $this->assertSame('default', $scope, 'Test environment configuration must not depend on current store.');
            return $this->values[$path] ?? null;
        });
        $encryptor = $this->createMock(EncryptorInterface::class);
        $encryptor->method('decrypt')->willReturnCallback(function ($value) {
            $this->assertSame('0:3:ZW5jcnlwdGVkLXRlc3Qta2V5', $value);
            return 'decrypted-test-key';
        });
        $this->environment = new Environment($config, $encryptor);
    }

    public function testTestModeSelectsUatCredentialsIncludingDecryptedKey(): void
    {
        $helper = $this->createMock(Data::class);
        $plugin = new HelperConfig($this->environment);
        foreach (['reseller_code' => 'test-code', 'reseller_key' => 'test-reseller', 'encryption_key' => 'decrypted-test-key'] as $field => $expected) {
            $this->assertSame($expected, $plugin->afterGetConfigValue($helper, 'production-secret', 'tbi_settings/general_settings/' . $field));
        }
        $this->assertSame('Inactive', $plugin->afterGetConfigValue($helper, 'Active', 'tbi_settings/general_settings/status'));
        $this->assertSame('2000', $plugin->afterGetConfigValue($helper, '65000', 'tbi_settings/label_settings/maximum_visible'));
    }

    public function testProductionUsesExistingConfigAndConfigurableUrl(): void
    {
        $this->values[Environment::CONNECTION . 'test_mode'] = '0';
        $this->values[Environment::CONNECTION . 'production_url'] = 'https://merchant-api.example.test/';
        $plugin = new HelperConfig($this->environment);
        $helper = $this->createMock(Data::class);
        $this->assertSame('configured-production-key', $plugin->afterGetConfigValue($helper, 'configured-production-key', 'tbi_settings/general_settings/encryption_key'));
        $this->assertSame('https://merchant-api.example.test/api/RegisterApplication', $this->environment->replaceVendorOrigin(Environment::VENDOR_ORIGIN . '/api/RegisterApplication'));
    }

    public function testMissingUatCredentialNeverFallsBackToProduction(): void
    {
        unset($this->values['tbi_settings/uat_settings/encryption_key']);
        $plugin = new HelperConfig($this->environment);
        $this->expectException(LocalizedException::class);
        $plugin->afterGetConfigValue($this->createMock(Data::class), 'production-secret', 'tbi_settings/general_settings/encryption_key');
    }

    public function testProductionEncryptedKeyIsDecodedAtHelperBoundary(): void
    {
        $this->values[Environment::CONNECTION . 'test_mode'] = '0';
        $plugin = new HelperConfig($this->environment);
        $this->assertSame('decrypted-test-key', $plugin->afterGetConfigValue(
            $this->createMock(Data::class),
            '0:3:ZW5jcnlwdGVkLXRlc3Qta2V5',
            'tbi_settings/general_settings/encryption_key'
        ));
    }

    public function testUatKeyAlreadyDecodedByMagentoIsNotDecryptedAgain(): void
    {
        $this->values['tbi_settings/uat_settings/encryption_key'] = 'already-decoded-uat-key';
        $plugin = new HelperConfig($this->environment);
        $this->assertSame('already-decoded-uat-key', $plugin->afterGetConfigValue(
            $this->createMock(Data::class),
            'production-secret',
            'tbi_settings/general_settings/encryption_key'
        ));
    }

    public function testApiRejectsIncompleteUatAccount(): void
    {
        unset($this->values['tbi_settings/uat_settings/encryption_key']);
        $this->expectException(LocalizedException::class);
        (new ApiEndpoint($this->environment))->beforePost(new Curl(), Environment::VENDOR_ORIGIN . '/api/RegisterApplication', '{}');
    }

    public function testApiCallsAndCallbacksUseTheSameSelectedAccount(): void
    {
        $api = new ApiEndpoint($this->environment);
        $params = ['reseller_code' => 'test-code', 'data' => 'opaque-encrypted-payload'];
        foreach (['RegisterApplication', 'PluginsSettings', 'PluginsSettings/status', 'GetApplicationStatus'] as $endpoint) {
            $this->assertSame([Environment::UAT_ORIGIN . '/api/' . $endpoint, $params], $api->beforePost(new Curl(), Environment::VENDOR_ORIGIN . '/api/' . $endpoint, $params));
        }
        $helper = new HelperConfig($this->environment);
        $this->assertSame('decrypted-test-key', $helper->afterGetConfigValue($this->createMock(Data::class), 'production-key', 'tbi_settings/general_settings/encryption_key'));
    }

    public function testUnrelatedHttpRequestsAndPublicAssetsAreUnchanged(): void
    {
        $api = new ApiEndpoint($this->environment);
        foreach (['https://example.test/api/test', Environment::VENDOR_ORIGIN . '.example.test/api/test', Environment::VENDOR_ORIGIN . '/fusion-pay-ro-tbi/magento2/info.json'] as $url) {
            $this->assertSame([$url], $api->beforeGet(new Curl(), $url));
        }
    }

    public function testCalculatorOriginsAndJsonUrlsAgreeWithApiEnvironment(): void
    {
        $frontend = new FrontendUrls($this->environment);
        $subject = new \stdClass();
        $this->assertSame(Environment::UAT_ORIGIN, base64_decode($frontend->afterGetOrigin($subject, base64_encode(Environment::VENDOR_ORIGIN))));
        $this->assertSame(Environment::UAT_ORIGIN, $frontend->afterGetAllowedOrigin($subject, Environment::VENDOR_ORIGIN));
        $calculator = Environment::VENDOR_ORIGIN . '/calculator/?code=test-code&key=test-reseller&amount=';
        $this->assertSame(Environment::UAT_ORIGIN . '/calculator/?code=test-code&key=test-reseller&amount=', base64_decode($frontend->afterGetCalculatorUrl($subject, base64_encode($calculator))));
        $json = new JsonUrl($this->environment);
        $result = $this->createMock(Json::class);
        $this->assertSame([['url' => Environment::UAT_ORIGIN . '/calculator/?amount=100'], false, []], $json->beforeSetData($result, ['url' => Environment::VENDOR_ORIGIN . '/calculator/?amount=100']));
        $data = ['url' => 'https://app-rotbi-envoy-fos24-uat.azurewebsites.net/b2cportal/example', 'token' => 'opaque'];
        $this->assertSame([$data, false, []], $json->beforeSetData($result, $data));
    }

    public function testEnvironmentChangesAreNotCachedInPlugins(): void
    {
        $api = new ApiEndpoint($this->environment);
        $url = Environment::VENDOR_ORIGIN . '/api/RegisterApplication';
        $this->assertSame(Environment::UAT_ORIGIN . '/api/RegisterApplication', $api->beforePost(new Curl(), $url, '{}')[0]);
        $this->values[Environment::CONNECTION . 'test_mode'] = '0';
        $this->assertSame($url, $api->beforePost(new Curl(), $url, '{}')[0]);
    }

    public function testSyncWritesUatStatusAndLimitsWithoutChangingProductionOrAdminSaves(): void
    {
        $sync = new SettingsSync($this->environment);
        $config = $this->createMock(Config::class);
        $subject = $this->createMock(AfterConfigSave::class);
        $observer = new Observer();
        $paths = ['tbi_settings/general_settings/status', 'tbi_settings/label_settings/tier1', 'tbi_settings/label_settings/last_api_reseller_key'];
        $sync->aroundExecute($subject, function ($received) use ($sync, $config, $observer, $paths) {
            $this->assertSame($observer, $received);
            foreach ($paths as $path) {
                $this->assertSame([$this->environment->getEffectivePath($path), 'value', 'default', 0], $sync->beforeSaveConfig($config, $path, 'value'));
            }
        }, $observer);
        foreach ($paths as $path) {
            $this->assertSame([$path, 'value', 'websites', 2], $sync->beforeSaveConfig($config, $path, 'value', 'websites', 2));
        }
    }

    public function testSyncContextIsResetOnFailure(): void
    {
        $sync = new SettingsSync($this->environment);
        try {
            $sync->aroundExecute($this->createMock(AfterConfigSave::class), function () {
                throw new \RuntimeException('simulated API failure');
            }, new Observer());
            $this->fail('Expected simulated failure.');
        } catch (\RuntimeException $exception) {
            $this->assertSame('simulated API failure', $exception->getMessage());
        }
        $path = 'tbi_settings/general_settings/reseller_code';
        $this->assertSame([$path, 'production', 'default', 0], $sync->beforeSaveConfig($this->createMock(Config::class), $path, 'production'));
    }

    /** @dataProvider invalidOrigins */
    public function testUnsafeOriginsAreRejected(string $url): void
    {
        $this->values[Environment::CONNECTION . 'uat_url'] = $url;
        $this->expectException(LocalizedException::class);
        $this->environment->getOrigin();
    }

    public static function invalidOrigins(): array
    {
        return [['http://example.test'], ['https://user:pass@example.test'], ['https://example.test/api'], ['https://example.test?key=value'], ['https://example.test#fragment'], ['invalid']];
    }

    public function testCspAddsOnlySelectedOriginAndPreservesExistingPolicies(): void
    {
        $policies = (new CspCollector($this->environment))->collect();
        $this->assertCount(2, $policies);
        foreach ($policies as $policy) {
            $this->assertSame([Environment::UAT_ORIGIN], $policy->getHostSources());
            $this->assertFalse($policy->isInlineAllowed());
        }
        $this->assertSame($policies[0], (new CspCollector($this->environment))->collect([$policies[0]])[0]);
    }
}
