<?php
namespace ExternalModules;
require_once 'BaseTest.php';

use \Exception;

class APITest extends BaseTest
{
    private static $itemIds = ['first' => '', 'second'=>''];
    private static $apiUser = '';
    private static $existingItems = [];
    private static $apiToken = '';
    private static $apiRequestParams = [
        'content'=>'externalModule',
        'prefix'=>'module-development-examples',
        'returnFormat'=>'json'
    ];
    private static $curlHandler = null;
    private static $curlConfig = [
        CURLOPT_URL            => APP_PATH_WEBROOT_FULL."api",
        CURLOPT_CUSTOMREQUEST => "POST",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTREDIR => 3,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_HTTPHEADER => array('Content-Type: application/x-www-form-urlencoded')
    ];
    private static $moduleEnabled = false;

    function __construct() {
        parent::__construct();
        // Make sure the test module is enabled on the REDCap system
        if (!ExternalModules::isModuleEnabled('module-development-examples')) {
            $result = ExternalModules::enableAndCatchExceptions('module-development-examples','v1.0');
        }
        // Make sure test module is enabled on the test project
        if (!self::$moduleEnabled) {
            $version = ExternalModules::getEnabledVersion('module-development-examples');
            ExternalModules::enableForProject('module-development-examples',$version,TEST_SETTING_PID);
            self::$moduleEnabled = true;
        }
    }

    function setUp(): void
    {
        parent::setUp();

        if($this->isRunningOnCI()){
            $this->markTestSkipped('This test is not currently supported in GitHub actions because we do not currently setup apache within them.');
        };
    }

    private function setupForApiTests() {
        // Initializing curl session for tests
        self::$curlHandler = curl_init();
        //Create the api token for this test
        self::$apiToken = 'apitesttoken'.bin2hex(random_bytes(6));

        //Check for this being the Github installation and setting the default user to not be an admin. This is required to properly run the API tests.
        if ($this->isRunningOnCI()) {
            $sql = "update redcap_user_information set super_user = ?, admin_rights = ? WHERE username=?";
            ExternalModules::query($sql,[0,0,'site_admin']);
        }

        // Let's grab a random non-admin user for checks in our tests
        self::$apiUser = $this->getRandomUsername(true);
        // Add user to the test project after making sure it doesn't already exist
        \UserRights::removePrivileges(TEST_SETTING_PID, self::$apiUser);
        $q = \UserRights::addPrivileges(TEST_SETTING_PID,['username'=>self::$apiUser,'forms'=>[],'expiration'=>"",'forms_export'=>[],'api_modules'=>1]);
        if (!$q) {
            throw new Exception("Failed to add test user '".self::$apiUser."' to the test project ".TEST_SETTING_PID.".");
        }
        // Create our fake API token for our user
        $tokenResult = ExternalModules::query("update redcap_user_rights
            set api_token = ?
            where project_id = ? and username = ?",[self::$apiToken,TEST_SETTING_PID,self::$apiUser]);

        // CLeaning up any potential existing items from other failed tests
        list($result, $curlResponse) = $this->runAPICall('list-items', self::$apiToken, '', '');
		if (!is_array($result) || array_key_exists("error", $result)) {
			throw new Exception("Failed to set up module API rights for test user '".self::$apiUser." with token ".self::$apiToken."'. Result was ".json_encode($result).". Curl response is ".json_encode($curlResponse));
		}
        foreach ($result as $existingItem) {
            if ($existingItem['item-id'] != '') {
                list($subresult, $subcurlResponse) = $this->runAPICall('remove-item', self::$apiToken, $existingItem['item-id'], '');
            }
        }
    }

    private function tearDownAfterApiTests()
    {
        // Switch back to the valid API endpoint
        self::$curlConfig[CURLOPT_URL] = APP_PATH_WEBROOT_FULL . "api";
        // Cleaning up all existing items
        try {
            foreach (self::$existingItems as $existingItem) {
                if ($existingItem != '') {
                    list($result, $curlResponse) = $this->runAPICall('remove-item', self::$apiToken, $existingItem, '');
                }
            }
        }
        catch (\Throwable $e) {
            // Do nothing
        }
        // Remove our test user from the test project
        \UserRights::removePrivileges(TEST_SETTING_PID, self::$apiUser);

        curl_close(self::$curlHandler);
    }

    function testModuleAPI()
    {
        try {
            $this->setupForApiTests();

            // Add first item
            list($result, $curlResponse) = self::runAPICall('add-item', self::$apiToken, '', 'My first item!');
            $this->assertArrayHasKey('item-id',$result);
            self::$itemIds['first'] = $result["item-id"] ?? '';
    
            // Add second item
            list($result, $curlResponse) = self::runAPICall('add-item', self::$apiToken, '', 'My second item!');
            $this->assertArrayHasKey('item-id',$result);
            self::$itemIds['second'] = $result["item-id"] ?? '';
    
            // List all items
            list($result, $curlResponse) = self::runAPICall('list-items', self::$apiToken, '', '');
            $this->assertSame(count(self::$itemIds),count($result));
    
            // Get a specific item by ID
            list($result, $curlResponse) = self::runAPICall('get-item', self::$apiToken, self::$itemIds['first'], '');
            $this->assertArrayNotHasKey('error',$result,$result['error']??"");
    
            // Remove the first item that was added
            list($result, $curlResponse) = self::runAPICall('remove-item', self::$apiToken, self::$itemIds['first'], '');
            $this->assertArrayNotHasKey('error',$result ?? [],$result['error']??"");
    
            // List items now that one was removed
            list($result, $curlResponse) = self::runAPICall('list-items', self::$apiToken, '', '');
            $this->assertSame((count(self::$itemIds) - 1),count($result));
            foreach ($result as $item) {
                if (isset($item['item-id'])) {
                    self::$existingItems[] = $item['item-id'];
                }
            }
    
            // Unauthenticated item lists
            list($result,$curlResponse) = self::runAPICall('list-items','','','');
            $this->assertSame((count(self::$itemIds) - 1),count($result));
    
            // Unauthenticated retrieval of single item
            list($result,$curlResponse) = self::runAPICall('get-item','',self::$itemIds['second'],'');
            $this->assertArrayNotHasKey('error',$result,$result['error']??"");
    
            // Unauthenticated item removal (Should fail!)
            list($result,$curlResponse) = self::runAPICall('remove-item','',self::$itemIds['second'],'');
            $this->assertSame("The requested action requires authentication.",$result['error'] ?? []);
    
            // Attempting to call an API action that doesn't exist
            list($result,$curlResponse) = self::runAPICall('not-real-action',self::$apiToken,'','');
            $this->assertSame("The requested action is not specified in the '" . ExternalModules::MODULE_API_ACTIONS_SETTING . "' array in 'config.json'!",$result['error'] ?? []);
    
            // Attempting to call an invalid API endpoint
            self::$curlConfig[CURLOPT_URL] = APP_PATH_WEBROOT_FULL."not-api-real";
            list($result,$curlResponse) = self::runAPICall('list-items',self::$apiToken,'','');
            $this->assertSame(404,$curlResponse['http_code'] ?? []);
        }
        finally {
            $this->tearDownAfterApiTests();
        }
    }

    function runAPICall($action, $token, $id, $name):array {
        $data = self::$apiRequestParams;
        $data['action'] = $action;
        if ($name != '') $data['item-name'] = $name;
        if ($id != '') $data['item-id'] = $id;
        if ($token != '') $data['token'] = $token;
        $data_string = http_build_query($data);
        self::$curlConfig[CURLOPT_POSTFIELDS] = $data_string;
        curl_setopt_array(self::$curlHandler, self::$curlConfig);
        $output = json_decode(curl_exec(self::$curlHandler),true);
        $curlInfo = curl_getinfo(self::$curlHandler);

        return [$output, $curlInfo];
    }
}