<?php
/*
 *
 * Copyright 2018 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

use PHPUnit\Framework\TestCase;

class ChannelTest extends TestCase
{
    private Grpc\Gcp\GcpExtensionChannel $channel;
    private Grpc\Gcp\GcpExtensionChannel $channel1;
    private Grpc\Gcp\GcpExtensionChannel $channel2;
    private Grpc\Gcp\GcpExtensionChannel $channel3;

    public function tearDown(): void
    {
        if (!empty($this->channel)) {
            $this->channel->close();
        }
    }

    public function testInsecureCredentials()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50000',
          ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $this->assertSame('Grpc\Gcp\GcpExtensionChannel', get_class($this->channel));
    }

    public function testGetConnectivityState()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50001',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $state = $this->channel->getConnectivityState();
        $this->assertEquals(0, $state);
    }

    public function testGetConnectivityStateWithInt()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50002',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $state = $this->channel->getConnectivityState(123);
        $this->assertEquals(0, $state);
    }

    public function testGetConnectivityStateWithString()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50003',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $state = $this->channel->getConnectivityState('hello');
        $this->assertEquals(0, $state);
    }

    public function testGetConnectivityStateWithBool()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50004',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $state = $this->channel->getConnectivityState(true);
        $this->assertEquals(0, $state);
    }

    public function testGetTarget()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50005',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $target = $this->channel->getTarget();
        $this->assertTrue(is_string($target));
    }

    public function testWatchConnectivityState()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50006',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $now = Grpc\Timeval::now();
        $deadline = $now->add(new Grpc\Timeval(100*1000));  // 100ms
        // we act as if 'CONNECTING'(=1) was the last state
        // we saw, so the default state of 'IDLE' should be delivered instantly
        $state = $this->channel->watchConnectivityState(3, $deadline);
        $this->assertTrue($state);
        unset($now);
        unset($deadline);
    }

    public function testClose()
    {
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50007',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $this->assertNotNull($this->channel);
        $this->channel->close();
    }

    public function testInvalidConstructorWithNull()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->channel = new Grpc\Gcp\GcpExtensionChannel();
        $this->assertNull($this->channel);
    }

    public function testInvalidConstructorWith()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50008', 'invalid');
        $this->assertNull($this->channel);
    }

    public function testInvalidCredentials()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50009',
            ['credentials' => new Grpc\Timeval(100)]);
    }

    public function testInvalidOptionsArray()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50010',
            ['abc' => []]);
    }

    public function testInvalidGetConnectivityStateWithArray()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50011',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $this->channel->getConnectivityState([]);
    }

    public function testInvalidWatchConnectivityState()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50012',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $this->channel->watchConnectivityState([]);
    }

    public function testInvalidWatchConnectivityState2()
    {
        $this->expectException(InvalidArgumentException::class);
        $this->channel = new Grpc\Gcp\GcpExtensionChannel('localhost:50013',
            ['credentials' => Grpc\ChannelCredentials::createInsecure()]);
        $this->channel->watchConnectivityState(1, 'hi');
    }


    public function assertConnecting($state)
    {
        $this->assertTrue($state == GRPC\CHANNEL_CONNECTING ||
            $state == GRPC\CHANNEL_TRANSIENT_FAILURE);
    }

    public function waitUntilNotIdle($channel)
    {
        for ($i = 0; $i < 10; $i++) {
            $now = Grpc\Timeval::now();
            $deadline = $now->add(new Grpc\Timeval(1000));
            if ($channel->watchConnectivityState(GRPC\CHANNEL_IDLE,
                $deadline)) {
                return true;
            }
        }
        $this->assertTrue(false);
    }

    public function testPersistentChannelSameHost()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50014', [
            "grpc_target_persist_bound" => 3,
        ]);
        // the underlying grpc channel is the same by default
        // when connecting to the same host
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50014', []);

        // both channels should be IDLE
        $state = $this->channel1->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        // both channels should now be in the CONNECTING state
        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertConnecting($state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelDifferentHost()
    {
        // two different underlying channels because different hostname
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50015', [
            "grpc_target_persist_bound" => 3,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50016', []);

        // both channels should be IDLE
        $state = $this->channel1->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        // channel1 should now be in the CONNECTING state
        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        // channel2 should still be in the IDLE state
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelSameArgs()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50017', [
            "grpc_target_persist_bound" => 3,
            "abc" => "def",
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50017', ["abc" => "def"]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertConnecting($state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelDifferentArgs()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50018', [
            "grpc_target_persist_bound" => 3,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50018', ["abc" => "def"]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelSameChannelCredentials()
    {
        $creds1 = Grpc\ChannelCredentials::createSsl();
        $creds2 = Grpc\ChannelCredentials::createSsl();

        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50019',
            ["credentials" => $creds1,
                "grpc_target_persist_bound" => 3,
            ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50019',
            ["credentials" => $creds2]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertConnecting($state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelDifferentChannelCredentials()
    {
        $creds1 = Grpc\ChannelCredentials::createSsl();
        $creds2 = Grpc\ChannelCredentials::createSsl(
            file_get_contents(dirname(__FILE__) . '/data/ca.pem'));

        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50020',
            ["credentials" => $creds1,
                "grpc_target_persist_bound" => 3,
            ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50020',
            ["credentials" => $creds2]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelSameChannelCredentialsRootCerts()
    {
        $creds1 = Grpc\ChannelCredentials::createSsl(
            file_get_contents(dirname(__FILE__) . '/data/ca.pem'));
        $creds2 = Grpc\ChannelCredentials::createSsl(
            file_get_contents(dirname(__FILE__) . '/data/ca.pem'));

        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50021',
            ["credentials" => $creds1,
                "grpc_target_persist_bound" => 3,
            ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50021',
            ["credentials" => $creds2]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertConnecting($state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelDifferentSecureChannelCredentials()
    {
        $creds1 = Grpc\ChannelCredentials::createSsl();
        $creds2 = Grpc\ChannelCredentials::createInsecure();

        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50022',
            ["credentials" => $creds1,
                "grpc_target_persist_bound" => 3,
            ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50022',
            ["credentials" => $creds2]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelSharedChannelClose1()
    {
        // same underlying channel
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50123', [
            "grpc_target_persist_bound" => 3,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50123', []);

        // close channel1
        $this->channel1->close();

        // channel2 can still be use. We need to exclude the possible that
        // in testPersistentChannelSharedChannelClose2, the exception is thrown
        // by channel1.
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);
    }

    public function testPersistentChannelSharedChannelClose2()
    {
        $this->expectException(RuntimeException::class);
        // same underlying channel
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50223', [
            "grpc_target_persist_bound" => 3,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50223', []);

        // close channel1
        $this->channel1->close();

        // channel2 can still be use
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        // channel 1 is closed
        $state = $this->channel1->getConnectivityState();
    }

    public function testPersistentChannelCreateAfterClose()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50024', [
            "grpc_target_persist_bound" => 3,
        ]);

        $this->channel1->close();

        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50024', []);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel2->close();
    }

    public function testPersistentChannelSharedMoreThanTwo()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50025', [
            "grpc_target_persist_bound" => 3,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50025', []);
        $this->channel3 = new Grpc\Gcp\GcpExtensionChannel('localhost:50025', []);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        // all 3 channels should be in CONNECTING state
        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel3->getConnectivityState();
        $this->assertConnecting($state);

        $this->channel1->close();
    }

    public function callbackFunc($context)
    {
        return [];
    }

    public function callbackFunc2($context)
    {
        return ["k1" => "v1"];
    }

    public function testPersistentChannelWithCallCredentials()
    {
        $creds = Grpc\ChannelCredentials::createSsl();
        $callCreds = Grpc\CallCredentials::createFromPlugin(
            [$this, 'callbackFunc']);
        $credsWithCallCreds = Grpc\ChannelCredentials::createComposite(
            $creds, $callCreds);

        // If a ChannelCredentials object is composed with a
        // CallCredentials object, the underlying grpc channel will
        // always be created new and NOT persisted.
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50026',
            ["credentials" =>
                $credsWithCallCreds,
                "grpc_target_persist_bound" => 3,
            ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50026',
            ["credentials" =>
                $credsWithCallCreds]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelWithDifferentCallCredentials()
    {
        $callCreds1 = Grpc\CallCredentials::createFromPlugin(
            [$this, 'callbackFunc']);
        $callCreds2 = Grpc\CallCredentials::createFromPlugin(
            [$this, 'callbackFunc2']);

        $creds1 = Grpc\ChannelCredentials::createSsl();
        $creds2 = Grpc\ChannelCredentials::createComposite(
            $creds1, $callCreds1);
        $creds3 = Grpc\ChannelCredentials::createComposite(
            $creds1, $callCreds2);

        // Similar to the test above, anytime a ChannelCredentials
        // object is composed with a CallCredentials object, the
        // underlying grpc channel will always be separate and not
        // persisted
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50027',
            ["credentials" => $creds1,
                "grpc_target_persist_bound" => 3,
            ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50027',
            ["credentials" => $creds2]);
        $this->channel3 = new Grpc\Gcp\GcpExtensionChannel('localhost:50027',
            ["credentials" => $creds3]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);
        $state = $this->channel3->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
        $this->channel3->close();
    }

    public function testPersistentChannelForceNew()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50028', [
            "grpc_target_persist_bound" => 2,
        ]);
        // even though all the channel params are the same, channel2
        // has a new and different underlying channel
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50028',
            ["force_new" => true]);

        // try to connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelForceNewOldChannelIdle1()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50029', [
            "grpc_target_persist_bound" => 2,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50029',
            ["force_new" => true]);
        // channel3 shares with channel1
        $this->channel3 = new Grpc\Gcp\GcpExtensionChannel('localhost:50029', []);

        // try to connect on channel2
        $state = $this->channel2->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel2);
        $state = $this->channel1->getConnectivityState();

        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);
        $state = $this->channel2->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel3->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelForceNewOldChannelIdle2()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50029', [
            "grpc_target_persist_bound" => 2,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50029', []);

        // try to connect on channel2
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel2);
        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);
        $state = $this->channel2->getConnectivityState();
        $this->assertConnecting($state);

        $this->channel1->close();
        $this->channel2->close();
    }

    public function testPersistentChannelForceNewOldChannelClose1()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50130', [
            "grpc_target_persist_bound" => 2,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50130',
            ["force_new" => true]);
        // channel3 shares with channel1
        $this->channel3 = new Grpc\Gcp\GcpExtensionChannel('localhost:50130', []);

        $this->channel1->close();

        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        // channel3 is still usable. We need to exclude the possibility that in
        // testPersistentChannelForceNewOldChannelClose2, the exception is thrown
        // by channel1 and channel2.
        $state = $this->channel3->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);
    }

    public function testPersistentChannelForceNewOldChannelClose2()
    {
        $this->expectException(RuntimeException::class);
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50230', [
            "grpc_target_persist_bound" => 2,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50230',
            ["force_new" => true]);
        // channel3 shares with channel1
        $this->channel3 = new Grpc\Gcp\GcpExtensionChannel('localhost:50230', []);

        $this->channel1->close();

        $state = $this->channel2->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        // channel3 is still usable
        $state = $this->channel3->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        // channel 1 is closed
        $this->channel1->getConnectivityState();
    }

    public function testPersistentChannelForceNewNewChannelClose()
    {
        $this->channel1 = new Grpc\Gcp\GcpExtensionChannel('localhost:50031', [
            "grpc_target_persist_bound" => 2,
        ]);
        $this->channel2 = new Grpc\Gcp\GcpExtensionChannel('localhost:50031',
            ["force_new" => true]);
        $this->channel3 = new Grpc\Gcp\GcpExtensionChannel('localhost:50031', []);

        $this->channel2->close();

        $state = $this->channel1->getConnectivityState();
        $this->assertEquals(GRPC\CHANNEL_IDLE, $state);

        // can still connect on channel1
        $state = $this->channel1->getConnectivityState(true);
        $this->waitUntilNotIdle($this->channel1);

        $state = $this->channel1->getConnectivityState();
        $this->assertConnecting($state);

        $this->channel1->close();
    }
}
