<?php

namespace Tests\Icinga\Module\Director\Objects;

use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Data\PropertiesFilter\ArrayCustomVariablesFilter;
use Icinga\Module\Director\Data\PropertiesFilter\CustomVariablesFilter;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\Objects\DirectorDatafield;
use Icinga\Module\Director\Objects\IcingaHost;
use Icinga\Module\Director\Objects\IcingaHostGroup;
use Icinga\Module\Director\Objects\IcingaZone;
use Icinga\Module\Director\Test\BaseTestCase;
use Icinga\Exception\IcingaException;

class IcingaHostTest extends BaseTestCase
{
    protected $testHostName = '___TEST___host';
    protected $testDatafieldName = 'test5';

    public function testPropertiesCanBeSet()
    {
        $host = $this->host();
        $host->display_name = 'Something else';
        $this->assertEquals(
            $host->display_name,
            'Something else'
        );
    }

    public function testCanBeReplaced()
    {
        $host = $this->host();
        $newHost = IcingaHost::create(
            array('display_name' => 'Replaced display'),
            $this->getDb()
        );

        $this->assertEquals(
            count($host->vars()),
            4
        );
        $this->assertEquals(
            $host->address,
            '127.0.0.127'
        );

        $host->replaceWith($newHost);
        $this->assertEquals(
            $host->display_name,
            'Replaced display'
        );
        $this->assertEquals(
            $host->address,
            null
        );

        $this->assertEquals(
            count($host->vars()),
            0
        );
    }

    public function testCanBeMerged()
    {
        $host = $this->host();
        $newHost = IcingaHost::create(
            array('display_name' => 'Replaced display'),
            $this->getDb()
        );

        $this->assertEquals(
            count($host->vars()),
            4
        );
        $this->assertEquals(
            $host->address,
            '127.0.0.127'
        );

        $host->merge($newHost);
        $this->assertEquals(
            $host->display_name,
            'Replaced display'
        );
        $this->assertEquals(
            $host->address,
            '127.0.0.127'
        );
        $this->assertEquals(
            count($host->vars()),
            4
        );
    }

    public function testPropertiesCanBePreservedWhenBeingReplaced()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();
        $this->host()->store($db);
        $host = IcingaHost::load($this->testHostName, $db);

        $newHost = IcingaHost::create(
            array(
                'display_name'  => 'Replaced display',
                'address'       => '1.2.2.3',
                'vars'          => array(
                    'test1'     => 'newstring',
                    'test2'     => 18,
                    'initially' => 'set and then preserved',
                )
            ),
            $this->getDb()
        );

        $preserve = array('address', 'vars.test1', 'vars.initially');
        $host->replaceWith($newHost, $preserve);
        $this->assertEquals(
            $host->address,
            '127.0.0.127'
        );

        $this->assertEquals(
            $host->{'vars.test2'},
            18
        );

        $this->assertEquals(
            $host->vars()->test2->getValue(),
            18
        );

        $this->assertEquals(
            $host->{'vars.initially'},
            'set and then preserved'
        );

        $this->assertFalse(
            array_key_exists('address', $host->getModifiedProperties()),
            'Preserved property stays unmodified'
        );

        $newHost->set('vars.initially', 'changed later on');
        $newHost->set('vars.test2', 19);

        $host->replaceWith($newHost, $preserve);
        $this->assertEquals(
            $host->{'vars.initially'},
            'set and then preserved'
        );

        $this->assertEquals(
            $host->get('vars.test2'),
            19
        );


        $host->delete();
    }

    public function testDistinctCustomVarsCanBeSetWithoutSideEffects()
    {
        $host = $this->host();
        $host->set('vars.test2', 18);
        $this->assertEquals(
            $host->vars()->test1->getValue(),
            'string'
        );
        $this->assertEquals(
            $host->vars()->test2->getValue(),
            18
        );
        $this->assertEquals(
            $host->vars()->test3->getValue(),
            false
        );
    }

    public function testVarsArePersisted()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();
        $this->host()->store($db);
        $host = IcingaHost::load($this->testHostName, $db);
        $this->assertEquals(
            $host->vars()->test1->getValue(),
            'string'
        );
        $this->assertEquals(
            $host->vars()->test2->getValue(),
            17
        );
        $this->assertEquals(
            $host->vars()->test3->getValue(),
            false
        );
        $this->assertEquals(
            $host->vars()->test4->getValue(),
            (object) array(
                'this' => 'is',
                'a' => array(
                    'dict',
                    'ionary'
                )
            )
        );
    }

    public function testRendersCorrectly()
    {
        $this->assertEquals(
            (string) $this->host(),
            $this->loadRendered('host1')
        );
    }

    public function testGivesPlainObjectWithInvalidUnresolvedDependencies()
    {
        $props = $this->getDummyRelatedProperties();

        $host = $this->host();
        foreach ($props as $k => $v) {
            $host->$k = $v;
        }

        $plain = $host->toPlainObject();
        foreach ($props as $k => $v) {
            $this->assertEquals($plain->$k, $v);
        }
    }

    public function testCorrectlyStoresLazyRelations()
    {
        if ($this->skipForMissingDb()) {
            return;
        }
        $db = $this->getDb();
        $host = $this->host();
        $host->zone = '___TEST___zone';
        $this->assertEquals(
            '___TEST___zone',
            $host->zone
        );

        $zone = $this->newObject('zone', '___TEST___zone');
        $zone->store($db);

        $host->store($db);
        $host->delete();
        $zone->delete();
    }

    public function testFailsToStoreWithMissingLazyRelations()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $this->expectException(\RuntimeException::class);

        $db = $this->getDb();
        $host = $this->host();
        $host->zone = '___TEST___zone';
        $host->store($db);
    }

    public function testHandlesUnmodifiedProperties()
    {
        $this->markTestSkipped('Currently broken, needs to be fixed');

        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();
        $host = $this->host();
        $host->store($db);

        $parent = $this->newObject('host', '___TEST___parent');
        $parent->store($db);
        $host->imports = '___TEST___parent';

        $host->store($db);

        $plain = $host->getPlainUnmodifiedObject();
        $this->assertEquals(
            'string',
            $plain->vars->test1
        );
        $host->vars()->set('test1', 'nada');

        $host->store();

        $plain = $host->getPlainUnmodifiedObject();
        $this->assertEquals(
            'nada',
            $plain->vars->test1
        );

        $host->vars()->set('test1', 'string');
        $plain = $host->getPlainUnmodifiedObject();
        $this->assertEquals(
            'nada',
            $plain->vars->test1
        );

        $plain = $host->getPlainUnmodifiedObject();
        $test = IcingaHost::create((array) $plain);

        $this->assertEquals(
            $this->loadRendered('host3'),
            (string) $test
        );

        $host->delete();
        $parent->delete();
    }

    public function testRendersWithInvalidUnresolvedDependencies()
    {
        $newHost = $this->host();
        $newHost->zone             = 'invalid';
        $newHost->check_command    = 'unknown';
        $newHost->event_command    = 'What event?';
        $newHost->check_period     = 'Not time is a good time @ nite';
        $newHost->command_endpoint = 'nirvana';

        $this->assertEquals(
            (string) $newHost,
            $this->loadRendered('host2')
        );
    }

    public function testFailsToStoreWithInvalidUnresolvedDependencies()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $this->expectException(\RuntimeException::class);

        $host = $this->host();
        $host->zone = 'invalid';
        $host->store($this->getDb());
    }

    public function testRendersToTheCorrectZone()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();
        $host = $this->host()->setConnection($db);
        $masterzone = $db->getMasterZoneName();

        $config = new IcingaConfig($db);
        $host->renderToConfig($config);
        $this->assertEquals(
            array('zones.d/' . $masterzone . '/hosts.conf'),
            $config->getFileNames()
        );

        $zone = $this->newObject('zone', '___TEST___zone');
        $zone->store($db);

        $config = new IcingaConfig($db);
        $host->zone = '___TEST___zone';
        $host->renderToConfig($config);
        $this->assertEquals(
            array('zones.d/___TEST___zone/hosts.conf'),
            $config->getFileNames()
        );

        $host->has_agent = true;
        $host->master_should_connect = true;
        $host->accept_config = true;

        $config = new IcingaConfig($db);
        $host->renderToConfig($config);
        $this->assertEquals(
            array(
                'zones.d/___TEST___zone/hosts.conf',
                'zones.d/___TEST___zone/agent_endpoints.conf',
                'zones.d/___TEST___zone/agent_zones.conf'
            ),
            $config->getFileNames()
        );

        $host->object_type = 'template';
        $host->zone_id = null;

        $zone = $this->newObject('zone', '___TEST___zone2');
        $zone->store($db);

        $config = new IcingaConfig($db);
        $host->renderToConfig($config);
        $this->assertEquals(
            [
                'zones.d/___TEST___zone/host_templates.conf',
                'zones.d/___TEST___zone2/host_templates.conf',
            ],
            $config->getFileNames()
        );
    }

    public function testWhetherTwoHostsCannotBeStoredWithTheSameApiKey()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();
        $a = IcingaHost::create(array(
            'object_name' => '___TEST___a',
            'object_type' => 'object',
            'api_key' => 'a'
        ), $db);
        $b = IcingaHost::create(array(
            'object_name' => '___TEST___b',
            'object_type' => 'object',
            'api_key' => 'a'
        ), $db);

        $a->store();
        try {
            $b->store();
        } catch (\RuntimeException $e) {
            $msg = $e->getMessage();
            $matchMysql = strpos(
                $msg,
                "Duplicate entry 'a' for key 'api_key'"
            ) !== false;

            $matchPostgres = strpos(
                $msg,
                'Unique violation'
            ) !== false;

            $this->assertTrue(
                $matchMysql || $matchPostgres,
                'Exception message does not tell about unique constraint violation'
            );
            $a->delete();
        }
    }

    public function testWhetherHostCanBeLoadedWithValidApiKey()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();
        $a = IcingaHost::create(array(
            'object_name' => '___TEST___a',
            'object_type' => 'object',
            'api_key' => 'a1a1a1'
        ), $db);
        $b = IcingaHost::create(array(
            'object_name' => '___TEST___b',
            'object_type' => 'object',
            'api_key' => 'b1b1b1'
        ), $db);
        $a->store();
        $b->store();

        $this->assertEquals(
            IcingaHost::loadWithApiKey('b1b1b1', $db)->object_name,
            '___TEST___b'
        );

        $a->delete();
        $b->delete();
    }

    public function testWhetherInvalidApiKeyThrows404()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $this->expectException(NotFoundError::class);

        $db = $this->getDb();
        IcingaHost::loadWithApiKey('No___such___key', $db);
    }

    public function testEnumProperties()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();
        $properties = IcingaHost::enumProperties($db);

        $this->assertEquals(
            array(
                'Host properties' => $this->getDefaultHostProperties()
            ),
            $properties
        );
    }

    public function testEnumPropertiesWithCustomVars()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();

        $host = $this->host();
        $host->store($db);

        $properties = IcingaHost::enumProperties($db);
        $this->assertEquals(
            array(
                'Host properties' => $this->getDefaultHostProperties(),
                'Custom variables' => array(
                    'vars.test1' => 'test1',
                    'vars.test2' => 'test2',
                    'vars.test3' => 'test3',
                    'vars.test4' => 'test4'
                )
            ),
            $properties
        );
    }

    public function testEnumPropertiesWithPrefix()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();

        $host = $this->host();
        $host->store($db);

        $properties = IcingaHost::enumProperties($db, 'host.');
        $this->assertEquals(
            array(
                'Host properties' => $this->getDefaultHostProperties('host.'),
                'Custom variables' => array(
                    'host.vars.test1' => 'test1',
                    'host.vars.test2' => 'test2',
                    'host.vars.test3' => 'test3',
                    'host.vars.test4' => 'test4'
                )
            ),
            $properties
        );
    }

    public function testEnumPropertiesWithFilter()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();

        DirectorDatafield::create(array(
            'varname'       => $this->testDatafieldName,
            'caption'       => 'Blah',
            'description'   => '',
            'datatype'      => 'Icinga\Module\Director\DataType\DataTypeArray',
            'format'        => 'json'
        ))->store($db);

        $host = $this->host();
        $host->{'vars.test5'} = array('a', '1');
        $host->store($db);

        $properties = IcingaHost::enumProperties($db, '', new CustomVariablesFilter());
        $this->assertEquals(
            array(
                'Custom variables' => array(
                    'vars.test1' => 'test1',
                    'vars.test2' => 'test2',
                    'vars.test3' => 'test3',
                    'vars.test4' => 'test4',
                    'vars.test5' => 'test5 (Blah)'
                )
            ),
            $properties
        );
    }

    public function testEnumPropertiesWithArrayFilter()
    {
        if ($this->skipForMissingDb()) {
            return;
        }

        $db = $this->getDb();

        DirectorDatafield::create(array(
            'varname'       => $this->testDatafieldName,
            'caption'       => 'Blah',
            'description'   => '',
            'datatype'      => 'Icinga\Module\Director\DataType\DataTypeArray',
            'format'        => 'json'
        ))->store($db);

        $host = $this->host();
        $host->{'vars.test5'} = array('a', '1');
        $host->store($db);

        $properties = IcingaHost::enumProperties($db, '', new ArrayCustomVariablesFilter());
        $this->assertEquals(
            array(
                'Custom variables' => array(
                    'vars.test5' => 'test5 (Blah)'
                )
            ),
            $properties
        );
    }

    public function testMergingObjectKeepsGroupsIfNotGiven()
    {
        $one = IcingaHostGroup::create([
            'object_name' => 'one',
            'object_type' => 'object',
        ]);
        $two = IcingaHostGroup::create([
            'object_name' => 'two',
            'object_type' => 'object',
        ]);
        $a = IcingaHost::create([
            'object_name' => 'one',
            'object_type' => 'object',
            'imports'     => [],
            'address'     => '127.0.0.2',
            'groups'      => [$one, $two]
        ]);

        $b = IcingaHost::create([
            'object_name' => 'one',
            'object_type' => 'object',
            'imports'     => [],
            'address'     => '127.0.0.42',
        ]);

        $a->merge($b);
        $this->assertEquals(
            '127.0.0.42',
            $a->get('address')
        );
        $this->assertEquals(
            ['one', 'two'],
            $a->getGroups()
        );
    }

    protected function getDummyRelatedProperties()
    {
        return array(
            'zone'             => 'invalid',
            'check_command'    => 'unknown',
            'event_command'    => 'What event?',
            'check_period'     => 'Not time is a good time @ nite',
            'command_endpoint' => 'nirvana',
        );
    }

    protected function host()
    {
        return IcingaHost::create(array(
            'object_name'  => $this->testHostName,
            'object_type'  => 'object',
            'address'      => '127.0.0.127',
            'display_name' => 'Whatever',
            'vars'         => array(
                'test1' => 'string',
                'test2' => 17,
                'test3' => false,
                'test4' => (object) array(
                    'this' => 'is',
                    'a' => array(
                        'dict',
                        'ionary'
                    )
                )
            )
        ), $this->getDb());
    }

    protected function getDefaultHostProperties($prefix = '')
    {
        return array(
            "{$prefix}name" => "name",
            "{$prefix}action_url" => "action_url",
            "{$prefix}address" => "address",
            "{$prefix}address6" => "address6",
            "{$prefix}api_key" => "api_key",
            "{$prefix}check_command" => "check_command",
            "{$prefix}check_interval" => "check_interval",
            "{$prefix}check_period" => "check_period",
            "{$prefix}check_timeout" => "check_timeout",
            "{$prefix}command_endpoint" => "command_endpoint",
            "{$prefix}display_name" => "display_name",
            "{$prefix}enable_active_checks" => "enable_active_checks",
            "{$prefix}enable_event_handler" => "enable_event_handler",
            "{$prefix}enable_flapping" => "enable_flapping",
            "{$prefix}enable_notifications" => "enable_notifications",
            "{$prefix}enable_passive_checks" => "enable_passive_checks",
            "{$prefix}enable_perfdata" => "enable_perfdata",
            "{$prefix}event_command" => "event_command",
            "{$prefix}flapping_threshold_high" => "flapping_threshold_high",
            "{$prefix}flapping_threshold_low" => "flapping_threshold_low",
            "{$prefix}icon_image" => "icon_image",
            "{$prefix}icon_image_alt" => "icon_image_alt",
            "{$prefix}max_check_attempts" => "max_check_attempts",
            "{$prefix}notes" => "notes",
            "{$prefix}notes_url" => "notes_url",
            "{$prefix}retry_interval" => "retry_interval",
            "{$prefix}volatile" => "volatile",
            "{$prefix}zone" => "zone",
            "{$prefix}groups" => "Groups",
            "{$prefix}templates" => "templates"
        );
    }
    protected function loadRendered($name)
    {
        return file_get_contents(__DIR__ . '/rendered/' . $name . '.out');
    }

    public function tearDown(): void
    {
        if ($this->hasDb()) {
            $db = $this->getDb();
            $kill = array($this->testHostName, '___TEST___parent', '___TEST___a', '___TEST___b');
            foreach ($kill as $name) {
                if (IcingaHost::exists($name, $db)) {
                    IcingaHost::load($name, $db)->delete();
                }
            }

            $kill = ['___TEST___zone', '___TEST___zone2'];
            foreach ($kill as $name) {
                if (IcingaZone::exists($name, $db)) {
                    IcingaZone::load($name, $db)->delete();
                }
            }

            $this->deleteDatafields();
        }

        parent::tearDown();
    }

    protected function deleteDatafields()
    {
        $db = $this->getDb();
        $dbAdapter = $db->getDbAdapter();
        $kill = array($this->testDatafieldName);

        foreach ($kill as $name) {
            $query = $dbAdapter->select()
                ->from('director_datafield')
                ->where('varname = ?', $name);
            foreach (DirectorDatafield::loadAll($db, $query, 'id') as $datafield) {
                $datafield->delete();
            }
        }
    }
}
