# 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.

import os
from unittest import mock

import fixtures
from keystoneauth1 import identity
from keystoneauth1 import session

import openstack.config
from openstack import connection
from openstack import proxy
from openstack import service_description
from openstack.tests import fakes
from openstack.tests.unit import base
from openstack.tests.unit.fake import fake_service


CONFIG_AUTH_URL = "https://identity.example.com/"
CONFIG_USERNAME = "BozoTheClown"
CONFIG_PASSWORD = "TopSecret"
CONFIG_PROJECT = "TheGrandPrizeGame"
CONFIG_CACERT = "TrustMe"

CLOUD_CONFIG = f"""
clouds:
  sample-cloud:
    region_name: RegionOne
    auth:
      auth_url: {CONFIG_AUTH_URL}
      username: {CONFIG_USERNAME}
      password: {CONFIG_PASSWORD}
      project_name: {CONFIG_PROJECT}
  insecure-cloud:
    auth:
      auth_url: {CONFIG_AUTH_URL}
      username: {CONFIG_USERNAME}
      password: {CONFIG_PASSWORD}
      project_name: {CONFIG_PROJECT}
    cacert: {CONFIG_CACERT}
    verify: False
  insecure-cloud-alternative-format:
    auth:
      auth_url: {CONFIG_AUTH_URL}
      username: {CONFIG_USERNAME}
      password: {CONFIG_PASSWORD}
      project_name: {CONFIG_PROJECT}
    insecure: True
  cacert-cloud:
    auth:
      auth_url: {CONFIG_AUTH_URL}
      username: {CONFIG_USERNAME}
      password: {CONFIG_PASSWORD}
      project_name: {CONFIG_PROJECT}
    cacert: {CONFIG_CACERT}
  profiled-cloud:
    profile: dummy
    auth:
      username: {CONFIG_USERNAME}
      password: {CONFIG_PASSWORD}
      project_name: {CONFIG_PROJECT}
    cacert: {CONFIG_CACERT}
"""

VENDOR_CONFIG = f"""
{{
  "name": "dummy",
  "profile": {{
    "auth": {{
      "auth_url": "{CONFIG_AUTH_URL}"
    }},
    "vendor_hook": "openstack.tests.unit.test_connection:vendor_hook"
  }}
}}
"""

PUBLIC_CLOUDS_YAML = f"""
public-clouds:
  dummy:
    auth:
      auth_url: {CONFIG_AUTH_URL}
    vendor_hook: openstack.tests.unit.test_connection:vendor_hook
"""


class _TestConnectionBase(base.TestCase):
    def setUp(self):
        super().setUp()
        # Create a temporary directory where our test config will live
        # and insert it into the search path via OS_CLIENT_CONFIG_FILE.
        config_dir = self.useFixture(fixtures.TempDir()).path
        config_path = os.path.join(config_dir, "clouds.yaml")

        with open(config_path, "w") as conf:
            conf.write(CLOUD_CONFIG)

        self.useFixture(
            fixtures.EnvironmentVariable("OS_CLIENT_CONFIG_FILE", config_path)
        )
        self.use_keystone_v2()


class TestConnection(_TestConnectionBase):
    def test_other_parameters(self):
        conn = connection.Connection(cloud='sample-cloud', cert='cert')
        self.assertEqual(conn.session.cert, 'cert')

    def test_session_provided(self):
        mock_session = mock.Mock(spec=session.Session)
        mock_session.auth = identity.V3Password(
            auth_url='https://auth.example.com',
            password='passw0rd',
            user_id='fake',
        )
        conn = connection.Connection(session=mock_session, cert='cert')
        self.assertEqual(mock_session, conn.session)
        self.assertEqual('auth.example.com', conn.config.name)

    def test_create_session(self):
        conn = connection.Connection(cloud='sample-cloud')
        self.assertIsNotNone(conn)
        # TODO(mordred) Rework this - we need to provide requests-mock
        # entries for each of the proxies below
        # self.assertEqual('openstack.proxy',
        #                  conn.alarm.__class__.__module__)
        # self.assertEqual('openstack.clustering.v1._proxy',
        #                  conn.clustering.__class__.__module__)
        # self.assertEqual('openstack.compute.v2._proxy',
        #                  conn.compute.__class__.__module__)
        # self.assertEqual('openstack.database.v1._proxy',
        #                  conn.database.__class__.__module__)
        # self.assertEqual('openstack.identity.v2._proxy',
        #                  conn.identity.__class__.__module__)
        # self.assertEqual('openstack.image.v2._proxy',
        #                  conn.image.__class__.__module__)
        # self.assertEqual('openstack.object_store.v1._proxy',
        #                  conn.object_store.__class__.__module__)
        # self.assertEqual('openstack.load_balancer.v2._proxy',
        #                  conn.load_balancer.__class__.__module__)
        # self.assertEqual('openstack.orchestration.v1._proxy',
        #                  conn.orchestration.__class__.__module__)
        # self.assertEqual('openstack.workflow.v2._proxy',
        #                  conn.workflow.__class__.__module__)

    def test_create_unknown_proxy(self):
        self.register_uris(
            [
                self.get_placement_discovery_mock_dict(),
            ]
        )

        def closure():
            return self.cloud.placement

        self.assertIsInstance(self.cloud.placement, proxy.Proxy)
        self.assert_calls()

    def test_create_connection_version_param_default(self):
        c1 = connection.Connection(cloud='sample-cloud')
        conn = connection.Connection(session=c1.session)
        self.assertEqual(
            'openstack.identity.v3._proxy', conn.identity.__class__.__module__
        )

    def test_create_connection_version_param_string(self):
        c1 = connection.Connection(cloud='sample-cloud')
        conn = connection.Connection(
            session=c1.session, identity_api_version='2'
        )
        self.assertEqual(
            'openstack.identity.v2._proxy', conn.identity.__class__.__module__
        )

    def test_create_connection_version_param_int(self):
        c1 = connection.Connection(cloud='sample-cloud')
        conn = connection.Connection(
            session=c1.session, identity_api_version=3
        )
        self.assertEqual(
            'openstack.identity.v3._proxy', conn.identity.__class__.__module__
        )

    def test_create_connection_version_param_bogus(self):
        c1 = connection.Connection(cloud='sample-cloud')
        conn = connection.Connection(
            session=c1.session, identity_api_version='red'
        )
        # TODO(mordred) This is obviously silly behavior
        self.assertEqual(
            'openstack.identity.v3._proxy', conn.identity.__class__.__module__
        )

    def test_from_config_given_config(self):
        cloud_region = openstack.config.OpenStackConfig().get_one(
            "sample-cloud"
        )

        sot = connection.from_config(config=cloud_region)

        self.assertEqual(
            CONFIG_USERNAME, sot.config.config['auth']['username']
        )
        self.assertEqual(
            CONFIG_PASSWORD, sot.config.config['auth']['password']
        )
        self.assertEqual(
            CONFIG_AUTH_URL, sot.config.config['auth']['auth_url']
        )
        self.assertEqual(
            CONFIG_PROJECT, sot.config.config['auth']['project_name']
        )

    def test_from_config_given_cloud(self):
        sot = connection.from_config(cloud="sample-cloud")

        self.assertEqual(
            CONFIG_USERNAME, sot.config.config['auth']['username']
        )
        self.assertEqual(
            CONFIG_PASSWORD, sot.config.config['auth']['password']
        )
        self.assertEqual(
            CONFIG_AUTH_URL, sot.config.config['auth']['auth_url']
        )
        self.assertEqual(
            CONFIG_PROJECT, sot.config.config['auth']['project_name']
        )

    def test_from_config_given_cloud_config(self):
        cloud_region = openstack.config.OpenStackConfig().get_one(
            "sample-cloud"
        )

        sot = connection.from_config(cloud_config=cloud_region)

        self.assertEqual(
            CONFIG_USERNAME, sot.config.config['auth']['username']
        )
        self.assertEqual(
            CONFIG_PASSWORD, sot.config.config['auth']['password']
        )
        self.assertEqual(
            CONFIG_AUTH_URL, sot.config.config['auth']['auth_url']
        )
        self.assertEqual(
            CONFIG_PROJECT, sot.config.config['auth']['project_name']
        )

    def test_from_config_given_cloud_name(self):
        sot = connection.from_config(cloud_name="sample-cloud")

        self.assertEqual(
            CONFIG_USERNAME, sot.config.config['auth']['username']
        )
        self.assertEqual(
            CONFIG_PASSWORD, sot.config.config['auth']['password']
        )
        self.assertEqual(
            CONFIG_AUTH_URL, sot.config.config['auth']['auth_url']
        )
        self.assertEqual(
            CONFIG_PROJECT, sot.config.config['auth']['project_name']
        )

    def test_from_config_verify(self):
        sot = connection.from_config(cloud="insecure-cloud")
        self.assertFalse(sot.session.verify)

        sot = connection.from_config(cloud="cacert-cloud")
        self.assertEqual(CONFIG_CACERT, sot.session.verify)

    def test_from_config_insecure(self):
        # Ensure that the "insecure=True" flag implies "verify=False"
        sot = connection.from_config("insecure-cloud-alternative-format")
        self.assertFalse(sot.session.verify)


class TestOsloConfig(_TestConnectionBase):
    def test_from_conf(self):
        c1 = connection.Connection(cloud='sample-cloud')
        conn = connection.Connection(
            session=c1.session, oslo_conf=self._load_ks_cfg_opts()
        )
        # There was no config for keystone
        self.assertIsInstance(
            conn.identity, service_description._ServiceDisabledProxyShim
        )
        # But nova was in there
        self.assertEqual(
            'openstack.compute.v2._proxy', conn.compute.__class__.__module__
        )

    def test_from_conf_filter_service_types(self):
        c1 = connection.Connection(cloud='sample-cloud')
        conn = connection.Connection(
            session=c1.session,
            oslo_conf=self._load_ks_cfg_opts(),
            service_types={'orchestration', 'i-am-ignored'},
        )
        # There was no config for keystone
        self.assertIsInstance(
            conn.identity, service_description._ServiceDisabledProxyShim
        )
        # Nova was in there, but disabled because not requested
        self.assertIsInstance(
            conn.compute, service_description._ServiceDisabledProxyShim
        )


class TestNetworkConnection(base.TestCase):
    # Verify that if the catalog has the suffix we don't mess things up.
    def test_network_proxy(self):
        self.os_fixture.v3_token.remove_service('network')
        svc = self.os_fixture.v3_token.add_service('network')
        svc.add_endpoint(
            interface='public',
            url='https://network.example.com/v2.0',
            region='RegionOne',
        )
        self.use_keystone_v3()
        self.assertEqual(
            'openstack.network.v2._proxy',
            self.cloud.network.__class__.__module__,
        )
        self.assert_calls()
        self.assertEqual(
            "https://network.example.com/v2.0",
            self.cloud.network.get_endpoint(),
        )


class TestNetworkConnectionSuffix(base.TestCase):
    # We need to do the neutron adapter test differently because it needs
    # to actually get a catalog.

    def test_network_proxy(self):
        self.assertEqual(
            'openstack.network.v2._proxy',
            self.cloud.network.__class__.__module__,
        )
        self.assert_calls()
        self.assertEqual(
            "https://network.example.com/v2.0",
            self.cloud.network.get_endpoint(),
        )


class TestAuthorize(base.TestCase):
    def test_authorize_works(self):
        res = self.cloud.authorize()
        self.assertEqual('KeystoneToken-1', res)

    def test_authorize_failure(self):
        self.use_broken_keystone()

        self.assertRaises(
            openstack.exceptions.SDKException, self.cloud.authorize
        )


class TestNewService(base.TestCase):
    def test_add_service_v1(self):
        svc = self.os_fixture.v3_token.add_service('fake')
        svc.add_endpoint(
            interface='public',
            region='RegionOne',
            url=f'https://fake.example.com/v1/{fakes.PROJECT_ID}',
        )
        self.use_keystone_v3()
        conn = self.cloud

        service = fake_service.FakeService('fake')

        conn.add_service(service)

        # Ensure no discovery calls made
        self.assertEqual(0, len(self.adapter.request_history))

        self.register_uris(
            [
                dict(
                    method='GET',
                    uri='https://fake.example.com',
                    status_code=404,
                ),
                dict(
                    method='GET',
                    uri='https://fake.example.com/v1/',
                    status_code=404,
                ),
                dict(
                    method='GET',
                    uri=self.get_mock_url('fake'),
                    status_code=404,
                ),
            ]
        )

        self.assertEqual(
            'openstack.tests.unit.fake.v1._proxy',
            conn.fake.__class__.__module__,
        )
        self.assertTrue(conn.fake.dummy())

    def test_add_service_v2(self):
        svc = self.os_fixture.v3_token.add_service('fake')
        svc.add_endpoint(
            interface='public',
            region='RegionOne',
            url=f'https://fake.example.com/v2/{fakes.PROJECT_ID}',
        )
        self.use_keystone_v3()
        conn = self.cloud

        self.register_uris(
            [
                dict(
                    method='GET',
                    uri='https://fake.example.com',
                    status_code=404,
                ),
                dict(
                    method='GET',
                    uri='https://fake.example.com/v2/',
                    status_code=404,
                ),
                dict(
                    method='GET',
                    uri=self.get_mock_url('fake'),
                    status_code=404,
                ),
            ]
        )

        service = fake_service.FakeService('fake')

        conn.add_service(service)

        self.assertEqual(
            'openstack.tests.unit.fake.v2._proxy',
            conn.fake.__class__.__module__,
        )
        self.assertFalse(conn.fake.dummy())

    def test_replace_system_service(self):
        svc = self.os_fixture.v3_token.add_service('fake')
        svc.add_endpoint(
            interface='public',
            region='RegionOne',
            url=f'https://fake.example.com/v2/{fakes.PROJECT_ID}',
        )
        self.use_keystone_v3()
        conn = self.cloud

        # delete native dns service
        delattr(conn, 'dns')

        self.register_uris(
            [
                dict(
                    method='GET',
                    uri='https://fake.example.com',
                    status_code=404,
                ),
                dict(
                    method='GET',
                    uri='https://fake.example.com/v2/',
                    status_code=404,
                ),
                dict(
                    method='GET',
                    uri=self.get_mock_url('fake'),
                    status_code=404,
                ),
            ]
        )

        # add fake service with alias 'DNS'
        service = fake_service.FakeService('fake', aliases=['dns'])
        conn.add_service(service)

        # ensure dns service responds as we expect from replacement
        self.assertFalse(conn.dns.dummy())


def vendor_hook(conn):
    setattr(conn, 'test', 'test_val')


class TestVendorProfile(base.TestCase):
    def setUp(self):
        super().setUp()
        # Create a temporary directory where our test config will live
        # and insert it into the search path via OS_CLIENT_CONFIG_FILE.
        config_dir = self.useFixture(fixtures.TempDir()).path
        config_path = os.path.join(config_dir, "clouds.yaml")
        public_clouds = os.path.join(config_dir, "clouds-public.yaml")

        with open(config_path, "w") as conf:
            conf.write(CLOUD_CONFIG)

        with open(public_clouds, "w") as conf:
            conf.write(PUBLIC_CLOUDS_YAML)

        self.useFixture(
            fixtures.EnvironmentVariable("OS_CLIENT_CONFIG_FILE", config_path)
        )
        self.use_keystone_v2()

        self.config = openstack.config.loader.OpenStackConfig(
            vendor_files=[public_clouds]
        )

    def test_conn_from_profile(self):
        self.cloud = self.config.get_one(cloud='profiled-cloud')

        conn = connection.Connection(config=self.cloud)

        self.assertIsNotNone(conn)

    def test_hook_from_profile(self):
        self.cloud = self.config.get_one(cloud='profiled-cloud')

        conn = connection.Connection(config=self.cloud)

        self.assertEqual('test_val', conn.test)

    def test_hook_from_connection_param(self):
        conn = connection.Connection(
            cloud='sample-cloud',
            vendor_hook='openstack.tests.unit.test_connection:vendor_hook',
        )

        self.assertEqual('test_val', conn.test)

    def test_hook_from_connection_ignore_missing(self):
        conn = connection.Connection(
            cloud='sample-cloud',
            vendor_hook='openstack.tests.unit.test_connection:missing',
        )

        self.assertIsNotNone(conn)
