<?php

declare(strict_types=1);

namespace Drupal\Tests\netforum\Unit;

use Drupal\netforum\NetForumXmlServiceOptions;
use Drupal\netforum\xWeb\Generated\ClassMap;
use Drupal\netforum\xWeb\Generated\StructType\Authenticate;
use Drupal\netforum\xWeb\Generated\StructType\AuthorizationToken;
use Drupal\netforum\xWeb\Generated\StructType\GetDateTime;
use Drupal\netforum\xWeb\Generated\StructType\GetTimeZones;
use Drupal\netforum\xWeb\Generated\StructType\GetVersion;
use Drupal\netforum\xWeb\Generated\StructType\GetVersionResponse;
use Drupal\netforum\xWeb\Generated\StructType\TestConnection;
use Drupal\netforum\xWeb\Generated\StructType\VersionClass;
use Drupal\netforum\xWeb\InstanceAuthTokenHandler;
use Drupal\netforum\xWeb\NetForumXml;
use Drupal\Tests\netforum\Utilities\RandomText;
use Drupal\Tests\netforum\xWeb\AuthExpirationPolicy;
use Drupal\Tests\netforum\xWeb\NetForumXmlSettings;
use Drupal\Tests\netforum\xWeb\SoapServerManager;
use Drupal\Tests\UnitTestCase;

/**
 * Tests for NetForumXml SOAP Client.
 *
 * @group netforum
 */
class NetForumXmlTest extends UnitTestCase {

  private const WSDL_FILENAME = 'netForumXML.wsdl';

  /**
   * Test automatically authenticating with the NetForum xWeb service.
   *
   * @throws \Random\RandomException
   */
  public function testAutomaticAuthenticate(): void {
    $username = RandomText::getRandomText(4);
    $password = RandomText::getRandomText(8);
    $nfSettings = new NetForumXmlSettings($username, $password);
    $soapServerManager = new SoapServerManager($nfSettings);
    $soapServerManager->startSoapServer();

    $nfOptions = new NetForumXmlServiceOptions();

    // Calling GetVersion will require authentication, which should
    // automatically be handled.
    $authHandlerCalled = FALSE;
    $nfOptions->setClassMap(ClassMap::get())
      ->setWsdl(dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . self::WSDL_FILENAME)
      ->setEndpoint('http://127.0.0.1:' . $soapServerManager->getSoapServerPort())
      ->setUserAgent('TestAgent/1.0')
      ->setAuthTokenHandler(new InstanceAuthTokenHandler(function () use (&$authHandlerCalled, &$nfXml, $username, $password): bool {
        $authHandlerCalled = TRUE;

        /** @var \Drupal\netforum\xWeb\NetForumXml $nfXml */
        return $nfXml->Authenticate(new Authenticate($username, $password)) !== FALSE;
      }));
    $nfXml = new NetForumXml($nfOptions->getOptions());

    $getVersionResponse = $nfXml->GetVersion(new GetVersion());
    $this->assertNotFalse($getVersionResponse, 'GetVersion() should not return false.');
    if ($getVersionResponse instanceof GetVersionResponse) {
      $this->assertInstanceOf(VersionClass::class, $getVersionResponse->getGetVersionResult()
        ?->getVersion(), 'A version of NetForum should be returned.');
    }

    $this->assertTrue($authHandlerCalled, 'The authenticate handler was not called.');

    $soapServerManager->stopSoapServer();
  }

  /**
   * Test handling sliding token expiration.
   *
   * @throws \Random\RandomException
   */
  public function testSlidingTokenExpiration(): void {
    $this->testTokenExpiration(AuthExpirationPolicy::Sliding);
  }

  /**
   * Test handling auth token expiration based on the expiration policy.
   *
   * @throws \Random\RandomException
   */
  private function testTokenExpiration(AuthExpirationPolicy $authExpirationPolicy): void {
    $username = RandomText::getRandomText(4);
    $password = RandomText::getRandomText(8);
    $nfSettings = new NetForumXmlSettings($username, $password, $authExpirationPolicy, FALSE, [], [], 3);
    $soapServerManager = new SoapServerManager($nfSettings);
    $soapServerManager->startSoapServer();

    $authHandlerCount = 0;
    $nfOptions = new NetForumXmlServiceOptions();
    $nfOptions->setClassMap(ClassMap::get())
      ->setWsdl(dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . self::WSDL_FILENAME)
      ->setEndpoint('http://127.0.0.1:' . $soapServerManager->getSoapServerPort())
      ->setUserAgent('TestAgent/1.0')
      ->setAuthTokenHandler(new InstanceAuthTokenHandler(function () use (&$authHandlerCount, &$nfXml, $username, $password): bool {
        $authHandlerCount++;

        /** @var \Drupal\netforum\xWeb\NetForumXml $nfXml */
        return $nfXml->Authenticate(new Authenticate($username, $password)) !== FALSE;
      }));
    $nfXml = new NetForumXml($nfOptions->getOptions());

    $getVersionResponse = $nfXml->GetVersion(new GetVersion());
    $this->assertNotFalse($getVersionResponse, 'GetVersion() should not return false.');

    // The token is set to expire after 3 seconds, we sleep for 5 seconds.
    sleep(5);

    $getTimeZonesResponse = $nfXml->GetTimeZones(new GetTimeZones());
    $this->assertNotFalse($getTimeZonesResponse, 'GetTimeZones() should not return false.');

    $this->assertEquals(2, $authHandlerCount, 'The authenticate handler should be called 2 times due to the token expiring between calls.');

    $soapServerManager->stopSoapServer();
  }

  /**
   * Test handling sliding tokens.
   *
   * @throws \Random\RandomException
   */
  public function testSlidingToken(): void {
    $username = RandomText::getRandomText(4);
    $password = RandomText::getRandomText(8);
    $nfSettings = new NetForumXmlSettings($username, $password, AuthExpirationPolicy::Sliding);
    $soapServerManager = new SoapServerManager($nfSettings);
    $soapServerManager->startSoapServer();

    $nfOptions = new NetForumXmlServiceOptions();
    $nfOptions->setClassMap(ClassMap::get())
      ->setWsdl(dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . self::WSDL_FILENAME)
      ->setEndpoint('http://127.0.0.1:' . $soapServerManager->getSoapServerPort())
      ->setUserAgent('TestAgent/1.0')
      ->setAuthTokenHandler(new InstanceAuthTokenHandler(function () use (&$nfXml, $username, $password): bool {
        /** @var \Drupal\netforum\xWeb\NetForumXml $nfXml */
        return $nfXml->Authenticate(new Authenticate($username, $password)) !== FALSE;
      }));
    $nfXml = new NetForumXml($nfOptions->getOptions());

    $authenticateResponse = $nfXml->Authenticate(new Authenticate($username, $password));
    $this->assertNotFalse($authenticateResponse, 'Authenticate() should not return false.');
    $authenticateResponseToken = self::getTokenFromSoapHeaders($nfXml->getOutputHeaders());
    $this->assertNotEmpty($authenticateResponseToken, 'The token should exist.');

    $getVersionResponse = $nfXml->GetVersion(new GetVersion());
    $this->assertNotFalse($getVersionResponse, 'GetVersion() should not return false.');
    $versionResponseToken = self::getTokenFromSoapHeaders($nfXml->getOutputHeaders());
    $this->assertNotEmpty($versionResponseToken, 'The token should exist.');

    // The tokens should be different due to sliding expiration.
    $this->assertNotSame($authenticateResponseToken, $versionResponseToken, 'The tokens should be different due to sliding expiration.');

    $soapServerManager->stopSoapServer();
  }

  private static function getTokenFromSoapHeaders(array $soapHeaders): ?string {
    foreach ($soapHeaders as $header) {
      if ($header instanceof AuthorizationToken) {
        return $header->getToken();
      }
    }

    return NULL;
  }

  /**
   * Test handling absolute token expiration.
   *
   * @throws \Random\RandomException
   */
  public function testAbsoluteTokenExpiration(): void {
    $this->testTokenExpiration(AuthExpirationPolicy::Absolute);
  }

  /**
   * Test calling a mix of Auth required and not required functions with
   * Sliding Authentication Expiration.
   *
   * @throws \Random\RandomException
   */
  public function testMixedAuthFunctionCallsSliding(): void {
    $this->testMixedAuthFunctionCalls(AuthExpirationPolicy::Sliding);
  }

  /**
   * Test calling xWeb functions that require Auth and then ones that don't
   * with the same instance of the NetForumXml client to ensure that
   * Authentication token management is working correctly.
   *
   * @throws \Random\RandomException
   */
  private function testMixedAuthFunctionCalls(AuthExpirationPolicy $authExpirationPolicy): void {
    $username = RandomText::getRandomText(4);
    $password = RandomText::getRandomText(8);
    $nfSettings = new NetForumXmlSettings($username, $password, $authExpirationPolicy);
    $soapServerManager = new SoapServerManager($nfSettings);
    $soapServerManager->startSoapServer();

    $nfOptions = new NetForumXmlServiceOptions();
    $nfOptions->setClassMap(ClassMap::get())
      ->setWsdl(dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . self::WSDL_FILENAME)
      ->setEndpoint('http://127.0.0.1:' . $soapServerManager->getSoapServerPort())
      ->setUserAgent('TestAgent/1.0')
      ->setAuthTokenHandler(new InstanceAuthTokenHandler(function () use (&$nfXml, $username, $password): bool {
        /** @var \Drupal\netforum\xWeb\NetForumXml $nfXml */
        return $nfXml->Authenticate(new Authenticate($username, $password)) !== FALSE;
      }));
    $nfXml = new NetForumXml($nfOptions->getOptions());

    // Requires Auth.
    $getVersionResponse = $nfXml->GetVersion(new GetVersion());
    $this->assertNotFalse($getVersionResponse, 'GetVersion() should not return false.');

    // Does not require Auth.
    $testConnectionResponse = $nfXml->TestConnection(new TestConnection());
    $this->assertNotFalse($testConnectionResponse, 'TestConnection() should not return false.');

    // Requires Auth.
    $getTimeZonesResponse = $nfXml->GetTimeZones(new GetTimeZones());
    $this->assertNotFalse($getTimeZonesResponse, 'GetTimeZones() should not return false.');

    $nfOptions2 = new NetForumXmlServiceOptions();
    $nfOptions2->setClassMap(ClassMap::get())
      ->setWsdl(dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . self::WSDL_FILENAME)
      ->setEndpoint('http://127.0.0.1:' . $soapServerManager->getSoapServerPort())
      ->setUserAgent('TestAgent/1.0')
      ->setAuthTokenHandler(new InstanceAuthTokenHandler(function () use (&$nfXml2, $username, $password): bool {
        /** @var \Drupal\netforum\xWeb\NetForumXml $nfXml2 */
        return $nfXml2->Authenticate(new Authenticate($username, $password)) !== FALSE;
      }));
    $nfXml2 = new NetForumXml($nfOptions2->getOptions());

    // Does not require Auth.
    $testConnectionResponse = $nfXml2->TestConnection(new TestConnection());
    $this->assertNotFalse($testConnectionResponse, 'TestConnection() should not return false.');

    // Requires Auth.
    $getVersionResponse = $nfXml2->GetVersion(new GetVersion());
    $this->assertNotFalse($getVersionResponse, 'GetVersion() should not return false.');

    // Does not require Auth.
    $getDateTimeResponse = $nfXml2->GetDateTime(new GetDateTime());
    $this->assertNotFalse($getDateTimeResponse, 'GetDateTime() should not return false.');

    $soapServerManager->stopSoapServer();
  }

  /**
   * Test calling a mix of Auth required and not required functions with
   * Absolute Authentication Expiration.
   *
   * @throws \Random\RandomException
   */
  public function testMixedAuthFunctionCallsAbsolute(): void {
    $this->testMixedAuthFunctionCalls(AuthExpirationPolicy::Absolute);
  }

}
