<?php

namespace Telnyx;

/**
 * @internal
 * @covers \Telnyx\TelnyxObject
 */
final class TelnyxObjectTest extends \Telnyx\TestCase
{
    /** @var \ReflectionMethod */
    private $deepCopyReflector;

    /** @var \ReflectionMethod */
    private $optsReflector;

    /**
     * @before
     */
    public function setUpReflectors()
    {
        // Sets up reflectors needed by some tests to access protected or
        // private attributes.

        // This is used to invoke the `deepCopy` protected function
        $this->deepCopyReflector = new \ReflectionMethod(\Telnyx\TelnyxObject::class, 'deepCopy');
        $this->deepCopyReflector->setAccessible(true);

        // This is used to access the `_opts` protected variable
        $this->optsReflector = new \ReflectionProperty(\Telnyx\TelnyxObject::class, '_opts');
        $this->optsReflector->setAccessible(true);
    }

    public function testArrayAccessorsSemantics()
    {
        $s = new TelnyxObject();
        $s['foo'] = 'a';
        static::assertSame($s['foo'], 'a');
        static::assertTrue(isset($s['foo']));
        unset($s['foo']);
        static::assertFalse(isset($s['foo']));
    }

    public function testNormalAccessorsSemantics()
    {
        $s = new TelnyxObject();
        $s->foo = 'a';
        static::assertSame($s->foo, 'a');
        static::assertTrue(isset($s->foo));
        $s->foo = null;
        static::assertFalse(isset($s->foo));
    }

    public function testArrayAccessorsMatchNormalAccessors()
    {
        $s = new TelnyxObject();
        $s->foo = 'a';
        static::assertSame($s['foo'], 'a');

        $s['bar'] = 'b';
        static::assertSame($s->bar, 'b');
    }

    public function testCount()
    {
        $s = new TelnyxObject();
        static::assertCount(0, $s);

        $s['key1'] = 'value1';
        static::assertCount(1, $s);

        $s['key2'] = 'value2';
        static::assertCount(2, $s);

        unset($s['key1']);
        static::assertCount(1, $s);
    }

    public function testKeys()
    {
        $s = new TelnyxObject();
        $s->foo = 'bar';
        static::assertSame($s->keys(), ['foo']);
    }

    public function testValues()
    {
        $s = new TelnyxObject();
        $s->foo = 'bar';
        static::assertSame($s->values(), ['bar']);
    }

    public function testToArray()
    {
        $array = [
            'foo' => 'a',
            'list' => [1, 2, 3],
            'null' => null,
            'metadata' => [
                'key' => 'value',
                1 => 'one',
            ],
        ];
        $s = TelnyxObject::constructFrom($array);

        $converted = $s->toArray();

        static::assertInternalType('array', $converted);
        static::assertSame($array, $converted);
    }

    public function testToArrayRecursive()
    {
        // deep nested associative array (when contained in an indexed array)
        // or TelnyxObject
        $nestedArray = ['id' => 7, 'foo' => 'bar'];
        $nested = TelnyxObject::constructFrom($nestedArray);

        $obj = TelnyxObject::constructFrom([
            'id' => 1,
            // simple associative array that contains a TelnyxObject to help us
            // test deep recursion
            'nested' => ['object' => 'list', 'data' => [$nested]],
            'list' => [$nested],
        ]);

        $expected = [
            'id' => 1,
            'nested' => ['object' => 'list', 'data' => [$nestedArray]],
            'list' => [$nestedArray],
        ];

        static::assertSame($expected, $obj->toArray());
    }

    public function testNonexistentProperty()
    {
        $capture = \tmpfile();
        $origErrorLog = \ini_set('error_log', \stream_get_meta_data($capture)['uri']);

        try {
            $s = new TelnyxObject();
            static::assertNull($s->nonexistent);

            static::assertRegExp(
                '/Telnyx Notice: Undefined property of Telnyx\\\\TelnyxObject instance: nonexistent/',
                \stream_get_contents($capture)
            );
        } finally {
            \ini_set('error_log', $origErrorLog);
            \fclose($capture);
        }
    }

    public function testPropertyDoesNotExists()
    {
        $s = new TelnyxObject();
        static::assertNull($s['nonexistent']);
    }

    public function testJsonEncode()
    {
        $s = new TelnyxObject();
        $s->foo = 'a';

        static::assertSame('{"foo":"a"}', \json_encode($s));
    }

    public function testToString()
    {
        $s = new TelnyxObject();
        $s->foo = 'a';

        $string = (string) $s;
        $expected = "Telnyx\TelnyxObject JSON: {\n    \"foo\": \"a\"\n}";
        static::assertSame($expected, $string);
    }

    public function testReplaceNewNestedUpdatable()
    {
        $s = new TelnyxObject();

        $s->metadata = ['bar'];
        static::assertSame($s->metadata, ['bar']);
        $s->metadata = ['baz', 'qux'];
        static::assertSame($s->metadata, ['baz', 'qux']);
    }

    public function testSetPermanentAttribute()
    {
        $this->expectException(\InvalidArgumentException::class);

        $s = new TelnyxObject();
        $s->id = 'abc_123';
    }

    public function testSetEmptyStringValue()
    {
        $this->expectException(\InvalidArgumentException::class);

        $s = new TelnyxObject();
        $s->foo = '';
    }

    public function testSerializeParametersOnEmptyObject()
    {
        $obj = TelnyxObject::constructFrom([]);
        static::assertSame([], $obj->serializeParameters());
    }

    public function testSerializeParametersOnNewObjectWithSubObject()
    {
        $obj = new TelnyxObject();
        $obj->metadata = ['foo' => 'bar'];
        static::assertSame(['metadata' => ['foo' => 'bar']], $obj->serializeParameters());
    }

    public function testSerializeParametersOnBasicObject()
    {
        $obj = TelnyxObject::constructFrom(['foo' => null]);
        $obj->updateAttributes(['foo' => 'bar']);
        static::assertSame(['foo' => 'bar'], $obj->serializeParameters());
    }

    public function testSerializeParametersOnMoreComplexObject()
    {
        $obj = TelnyxObject::constructFrom([
            'foo' => TelnyxObject::constructFrom([
                'bar' => null,
                'baz' => null,
            ]),
        ]);
        $obj->foo->bar = 'newbar';
        static::assertSame(['foo' => ['bar' => 'newbar']], $obj->serializeParameters());
    }

    public function testSerializeParametersOnArray()
    {
        $obj = TelnyxObject::constructFrom([
            'foo' => null,
        ]);
        $obj->foo = ['new-value'];
        static::assertSame(['foo' => ['new-value']], $obj->serializeParameters());
    }

    public function testSerializeParametersOnArrayThatShortens()
    {
        $obj = TelnyxObject::constructFrom([
            'foo' => ['0-index', '1-index', '2-index'],
        ]);
        $obj->foo = ['new-value'];
        static::assertSame(['foo' => ['new-value']], $obj->serializeParameters());
    }

    public function testSerializeParametersOnArrayThatLengthens()
    {
        $obj = TelnyxObject::constructFrom([
            'foo' => ['0-index', '1-index', '2-index'],
        ]);
        $obj->foo = \array_fill(0, 4, 'new-value');
        static::assertSame(['foo' => \array_fill(0, 4, 'new-value')], $obj->serializeParameters());
    }

    public function testSerializeParametersOnArrayOfHashes()
    {
        $obj = TelnyxObject::constructFrom(['foo' => null]);
        $obj->foo = [
            TelnyxObject::constructFrom(['bar' => null]),
        ];

        $obj->foo[0]->bar = 'baz';
        static::assertSame(['foo' => [['bar' => 'baz']]], $obj->serializeParameters());
    }

    public function testSerializeParametersDoesNotIncludeUnchangedValues()
    {
        $obj = TelnyxObject::constructFrom([
            'foo' => null,
        ]);
        static::assertSame([], $obj->serializeParameters());
    }

    public function testSerializeParametersOnUnchangedArray()
    {
        $obj = TelnyxObject::constructFrom([
            'foo' => ['0-index', '1-index', '2-index'],
        ]);
        $obj->foo = ['0-index', '1-index', '2-index'];
        static::assertSame([], $obj->serializeParameters());
    }

    public function testSerializeParametersWithTelnyxObject()
    {
        $obj = TelnyxObject::constructFrom([]);
        $obj->metadata = TelnyxObject::constructFrom(['foo' => 'bar']);

        $serialized = $obj->serializeParameters();
        static::assertSame(['foo' => 'bar'], $serialized['metadata']);
    }

    public function testSerializeParametersOnReplacedTelnyxObject()
    {
        $obj = TelnyxObject::constructFrom([
            'source' => TelnyxObject::constructFrom(['bar' => 'foo']),
        ]);
        $obj->source = TelnyxObject::constructFrom(['baz' => 'foo']);

        $serialized = $obj->serializeParameters();
        static::assertSame(['baz' => 'foo'], $serialized['source']);
    }

    public function testSerializeParametersOnReplacedTelnyxObjectWhichIsMetadata()
    {
        $obj = TelnyxObject::constructFrom([
            'metadata' => TelnyxObject::constructFrom(['bar' => 'foo']),
        ]);
        $obj->metadata = TelnyxObject::constructFrom(['baz' => 'foo']);

        $serialized = $obj->serializeParameters();
        static::assertSame(['bar' => '', 'baz' => 'foo'], $serialized['metadata']);
    }

    public function testSerializeParametersOnArrayOfTelnyxObjects()
    {
        $obj = TelnyxObject::constructFrom([]);
        $obj->metadata = [
            TelnyxObject::constructFrom(['foo' => 'bar']),
        ];

        $serialized = $obj->serializeParameters();
        static::assertSame([['foo' => 'bar']], $serialized['metadata']);
    }

    public function testSerializeParametersOnSetApiResource()
    {
        $address = Address::constructFrom(['id' => 'cus_123']);
        $obj = TelnyxObject::constructFrom([]);

        // the key here is that the property is set explicitly (and therefore
        // marked as unsaved), which is why it gets included below
        $obj->address = $address;

        $serialized = $obj->serializeParameters();
        static::assertSame(['address' => $address], $serialized);
    }

    public function testSerializeParametersOnNotSetApiResource()
    {
        $address = Address::constructFrom(['id' => 'cus_123']);
        $obj = TelnyxObject::constructFrom(['address' => $address]);

        $serialized = $obj->serializeParameters();
        static::assertSame([], $serialized);
    }

    public function testSerializeParametersOnApiResourceFlaggedWithSaveWithParent()
    {
        $address = Address::constructFrom(['id' => 'cus_123']);
        $address->saveWithParent = true;

        $obj = TelnyxObject::constructFrom(['address' => $address]);

        $serialized = $obj->serializeParameters();
        static::assertSame(['address' => []], $serialized);
    }

    public function testSerializeParametersRaisesExceotionOnOtherEmbeddedApiResources()
    {
        // This address doesn't have an ID and therefore the library doesn't know
        // what to do with it and throws an InvalidArgumentException because it's
        // probably not what the user expected to happen.
        $address = Address::constructFrom([]);

        $obj = TelnyxObject::constructFrom([]);
        $obj->address = $address;

        try {
            $serialized = $obj->serializeParameters();
            static::fail('Did not raise error');
        } catch (\InvalidArgumentException $e) {
            static::assertSame(
                'Cannot save property `address` containing an API resource of type Telnyx\\Address. ' .
                "It doesn't appear to be persisted and is not marked as `saveWithParent`.",
                $e->getMessage()
            );
        } catch (\Exception $e) {
            static::fail('Unexpected exception: ' . \get_class($e));
        }
    }

    public function testSerializeParametersForce()
    {
        $obj = TelnyxObject::constructFrom([
            'id' => 'id',
            'metadata' => TelnyxObject::constructFrom([
                'bar' => 'foo',
            ]),
        ]);

        $serialized = $obj->serializeParameters(true);
        static::assertSame(['id' => 'id', 'metadata' => ['bar' => 'foo']], $serialized);
    }

    public function testDirty()
    {
        $obj = TelnyxObject::constructFrom([
            'id' => 'id',
            'metadata' => TelnyxObject::constructFrom([
                'bar' => 'foo',
            ]),
        ]);

        // note that `$force` and `dirty()` are for different things, but are
        // functionally equivalent
        $obj->dirty();

        $serialized = $obj->serializeParameters();
        static::assertSame(['id' => 'id', 'metadata' => ['bar' => 'foo']], $serialized);
    }

    public function testDeepCopy()
    {
        $opts = [
            'api_base' => Telnyx::$apiBase,
            'api_key' => 'apikey',
        ];
        $values = [
            'id' => 1,
            'name' => 'Telnyx',
            'arr' => [
                TelnyxObject::constructFrom(['id' => 'index0'], $opts),
                'index1',
                2,
            ],
            'map' => [
                '0' => TelnyxObject::constructFrom(['id' => 'index0'], $opts),
                '1' => 'index1',
                '2' => 2,
            ],
        ];

        $copyValues = $this->deepCopyReflector->invoke(null, $values);

        // we can't compare the hashes directly because they have embedded
        // objects which are different from each other
        static::assertSame($values['id'], $copyValues['id']);
        static::assertSame($values['name'], $copyValues['name']);
        static::assertSame(\count($values['arr']), \count($copyValues['arr']));

        // internal values of the copied TelnyxObject should be the same,
        // but the object itself should be new (hence the assertNotSame)
        static::assertSame($values['arr'][0]['id'], $copyValues['arr'][0]['id']);
        static::assertNotSame($values['arr'][0], $copyValues['arr'][0]);

        // likewise, the Util\RequestOptions instance in _opts should have
        // copied values but be a new instance
        static::assertSame(
            $this->optsReflector->getValue($values['arr'][0])->apiKey,
            $this->optsReflector->getValue($copyValues['arr'][0])->apiKey
        );
        static::assertNotSame(
            $this->optsReflector->getValue($values['arr'][0]),
            $this->optsReflector->getValue($copyValues['arr'][0])
        );

        // scalars however, can be compared
        static::assertSame($values['arr'][1], $copyValues['arr'][1]);
        static::assertSame($values['arr'][2], $copyValues['arr'][2]);

        // and a similar story with the hash
        static::assertSame($values['map']['0']['id'], $copyValues['map']['0']['id']);
        static::assertNotSame($values['map']['0'], $copyValues['map']['0']);
        static::assertNotSame(
            $this->optsReflector->getValue($values['arr'][0]),
            $this->optsReflector->getValue($copyValues['arr'][0])
        );
        static::assertSame(
            $this->optsReflector->getValue($values['map']['0'])->apiKey,
            $this->optsReflector->getValue($copyValues['map']['0'])->apiKey
        );
        static::assertNotSame(
            $this->optsReflector->getValue($values['map']['0']),
            $this->optsReflector->getValue($copyValues['map']['0'])
        );
        static::assertSame($values['map']['1'], $copyValues['map']['1']);
        static::assertSame($values['map']['2'], $copyValues['map']['2']);
    }

    public function testDeepCopyMaintainClass()
    {
        $phone_number = PhoneNumber::constructFrom(['id' => 1], null);
        $copyPhoneNumber = $this->deepCopyReflector->invoke(null, $phone_number);
        static::assertSame(\get_class($phone_number), \get_class($copyPhoneNumber));
    }

    public function testIsDeleted()
    {
        $obj = TelnyxObject::constructFrom([]);
        static::assertFalse($obj->isDeleted());

        $obj = TelnyxObject::constructFrom(['deleted' => false]);
        static::assertFalse($obj->isDeleted());

        $obj = TelnyxObject::constructFrom(['deleted' => true]);
        static::assertTrue($obj->isDeleted());
    }

    public function testDeserializeEmptyMetadata()
    {
        $obj = TelnyxObject::constructFrom([
            'metadata' => [],
        ]);

        static::assertInstanceOf(\Telnyx\TelnyxObject::class, $obj->metadata);
    }

    public function testDeserializeMetadataWithKeyNamedMetadata()
    {
        $obj = TelnyxObject::constructFrom([
            'metadata' => ['metadata' => 'value'],
        ]);

        static::assertInstanceOf(\Telnyx\TelnyxObject::class, $obj->metadata);
        static::assertSame('value', $obj->metadata->metadata);
    }
}