<?php


namespace Mainto\RpcServer\RpcClient;


use Closure;
use Exception;
use Illuminate\Support\Collection;
use Mainto\RpcServer\Exceptions\RpcRuntimeException;
use ReflectionClass;
use ReflectionException;
use Throwable;

class RpcMockClass extends RpcClass {
    /**
     * @var array
     */
    protected array $mockData = [];

    /**
     * @var array
     */
    protected array $mockResponseId = [];

    /**
     * @var ReflectionClass
     */
    protected ?ReflectionClass $mockClass = null;

    /**
     * RpcMockClass constructor.
     * @param $cacheKey
     * @param $serviceName
     * @param $className
     */
    public function __construct (string $serviceName, string $className) {
        parent::__construct(null, $serviceName, $className);

        $defaultDataClass = "Mainto\\Bridge\\Mock\\{$serviceName}\\MockProvider";
        if (class_exists($defaultDataClass)) {
            new $defaultDataClass($this, $serviceName, $className);
        }
    }

    /**
     * 使用设置的 value 来进行 mock
     *
     * @param string $methodName
     * @param $responseValue
     * @param int $responseId
     */
    public function setValueMock (string $methodName, $responseValue, int $responseId = 1) {
        $this->mockData[$methodName] = array_replace($this->mockData[$methodName] ?? [], [
            $responseId => value($responseValue),
        ]);
    }

    /**
     * 设置下次调用时 mock 使用的 responseId
     *
     * @param string $methodName
     * @param int|callable $responseId
     */
    public function setMockResponseId (string $methodName, $responseId) {
        if (!(($responseId instanceof Closure) || is_numeric($responseId)) && !isset($this->mockData[$methodName][$responseId])) {
            throw new RpcRuntimeException("unknown mock id: {$responseId}");
        }

        $this->mockResponseId[$methodName] = $responseId;
    }

    /**
     * 使用设置的 exception 来进行 mock
     *
     * @param string $methodName
     * @param int $errorCode
     * @param string|array $errorMessage
     * @param int $responseId
     */
    public function setErrorMock (string $methodName, int $errorCode, $errorMessage, int $responseId = 1) {
        $this->setCallbackMock($methodName, function ($args = []) use ($errorCode, $errorMessage) {
            throw new RpcRuntimeException($errorMessage, $errorCode);
        }, $responseId);
    }

    /**
     * 使用设置的 函数 来进行 mock
     *
     * @param string $methodName
     * @param callable $callback
     * @param int $responseId
     */
    public function setCallbackMock (string $methodName, callable $callback, int $responseId = 1) {
        $this->mockData[$methodName] = array_replace($this->mockData[$methodName] ?? [], [
            $responseId => $callback,
        ]);;
    }

    /**
     * 使用设置的 类 来进行 mock，类中的每个方法应该和调用同名，并必须接收一个参数，是传入的请求
     *
     * @param string $class
     * @throws ReflectionException
     */
    public function setMockClass (string $class) {
        $this->mockClass = new ReflectionClass($class);
    }

    /**
     * 获取指定的返回参数
     *
     * @param string $methodName
     * @param int $responseId
     * @return mixed
     */
    public function tryGetMockResponse (string $methodName, int $responseId = 1) {
        if (isset($this->mockData[$methodName])) {
            if (!isset($this->mockData[$methodName][$responseId])) {
                throw new RpcRuntimeException("unknown mock id: {$responseId}");
            }

            $mockData = $this->mockData[$methodName][$responseId];
            if ($mockData instanceof Closure) {
                return $mockData([]);
            }

            return $mockData;
        }

        return [];
    }

    /**
     * @param $name
     * @param $arguments
     * @return Collection|mixed|null
     * @throws ReflectionException
     * @throws Exception|Throwable
     */
    public function __call ($name, $arguments) {
        $reqArguments = $arguments;
        if (count($reqArguments) == 1) {
            $reqArguments = $reqArguments[0] ?? $reqArguments;
        }

        if ($this->mockClass && $this->mockClass->hasMethod($name)) {
            $mockInstance = $this->mockClass->newInstanceWithoutConstructor();
            $refMethod = $this->mockClass->getMethod($name);
            return $refMethod->invokeArgs($mockInstance, [$reqArguments]);
        } else if (isset($this->mockData[$name])) {
            $mockId = 1;

            if (isset($reqArguments['__mock_id__'])) {
                $mockId = $reqArguments['__mock_id__'];
            } else if (isset($this->mockResponseId[$name])) {
                $mockId = $this->mockResponseId[$name];

                if ($mockId instanceof Closure) {
                    $mockId = (int)$mockId($reqArguments);
                }
            }

            if (!isset($this->mockData[$name][$mockId])) {
                throw new RpcRuntimeException("unknown mock id: {$mockId}");
            }

            $mockData = $this->mockData[$name][$mockId];
            if ($mockData instanceof Closure) {
                return $mockData($reqArguments);
            }

            return $mockData;
        } else {
            if (!$this->client) {
                $this->client = self::getRemoteClient($this->serviceName);
            }

            print "[Warn] '{$this->serviceName}->{$this->className}::{$name}' has no mock, transform to rpc.\n";
            return parent::__call($name, $arguments);
        }
    }
}