# Copyright 2021 RedHat Inc.
# All Rights Reserved.
#
#    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.

from unittest import mock
import uuid

from cinderclient.apiclient import exceptions as apiclient_exception
from cinderclient import exceptions as cinder_exception
from oslo_config import cfg
from oslotest import base

from glance_store.common import cinder_utils

CONF = cfg.CONF


class FakeObject(object):
    def __init__(self, **kwargs):
        for name, value in kwargs.items():
            setattr(self, name, value)


class CinderUtilsTestCase(base.BaseTestCase):

    def setUp(self):
        super(CinderUtilsTestCase, self).setUp()
        CONF.register_opt(cfg.DictOpt('enabled_backends'))
        CONF.set_override('enabled_backends', 'fake:cinder')
        self.volume_api = cinder_utils.API()
        self.fake_client = FakeObject(attachments=FakeObject(
            create=mock.MagicMock(), delete=mock.MagicMock(),
            complete=mock.MagicMock(), update=mock.MagicMock(),
            show=mock.MagicMock()))
        self.fake_vol_id = uuid.uuid4()
        self.fake_attach_id = uuid.uuid4()
        self.fake_connector = {
            'platform': 'x86_64', 'os_type': 'linux', 'ip': 'fake_ip',
            'host': 'fake_host', 'multipath': False,
            'initiator': 'fake_initiator', 'do_local_attach': False,
            'uuid': '3e1a7217-104e-41c1-b177-a37c491129a0',
            'system uuid': '98755544-c749-40ed-b30a-a1cb27b2a46d',
            'nqn': 'fake_nqn'}
        # Error message template for volume status errors
        self.volume_status_error_msg = (
            'Volume {volume_id} status must be available or in-use or '
            'downloading to reserve, but the current status is '
            '{current_status}.')

    def test_attachment_create(self):
        self.volume_api.attachment_create(self.fake_client, self.fake_vol_id)
        self.fake_client.attachments.create.assert_called_once_with(
            self.fake_vol_id, None, mode=None)

    def test_attachment_create_with_connector_and_mountpoint(self):
        self.volume_api.attachment_create(
            self.fake_client, self.fake_vol_id,
            connector=self.fake_connector, mountpoint='fake_mountpoint')
        self.fake_connector['mountpoint'] = 'fake_mountpoint'
        self.fake_client.attachments.create.assert_called_once_with(
            self.fake_vol_id, self.fake_connector, mode=None)

    def test_attachment_create_client_exception(self):
        self.fake_client.attachments.create.side_effect = (
            cinder_exception.ClientException(code=1))
        self.assertRaises(
            cinder_exception.ClientException,
            self.volume_api.attachment_create,
            self.fake_client, self.fake_vol_id)

    @mock.patch('time.sleep', new=mock.Mock())
    def test_attachment_create_retries(self):

        fake_attach_id = '12345678-1234-5678-1234-567812345678'
        # Make create fail two times and succeed on the third attempt.
        # Use error messages that match the retry condition
        error_msg = self.volume_status_error_msg.format(
            volume_id=self.fake_vol_id, current_status='reserved')
        self.fake_client.attachments.create.side_effect = [
            cinder_exception.BadRequest(400, message=error_msg),
            cinder_exception.BadRequest(400, message=error_msg),
            fake_attach_id]

        # Make sure we get a clean result.
        fake_attachment_id = self.volume_api.attachment_create(
            self.fake_client, self.fake_vol_id)

        self.assertEqual(fake_attach_id, fake_attachment_id)
        # Assert that we called attachment create three times due to the
        # retry decorator.
        self.fake_client.attachments.create.assert_has_calls([
            mock.call(self.fake_vol_id, None, mode=None),
            mock.call(self.fake_vol_id, None, mode=None),
            mock.call(self.fake_vol_id, None, mode=None)])

    def test_attachment_get(self):
        self.volume_api.attachment_get(self.fake_client, self.fake_attach_id)
        self.fake_client.attachments.show.assert_called_once_with(
            self.fake_attach_id)

    def test_attachment_get_client_exception(self):
        self.fake_client.attachments.show.side_effect = (
            cinder_exception.ClientException(code=1))
        self.assertRaises(
            cinder_exception.ClientException,
            self.volume_api.attachment_get,
            self.fake_client, self.fake_attach_id)

    def test_attachment_update(self):
        self.volume_api.attachment_update(self.fake_client,
                                          self.fake_attach_id,
                                          self.fake_connector)
        self.fake_client.attachments.update.assert_called_once_with(
            self.fake_attach_id, self.fake_connector)

    def test_attachment_update_with_connector_and_mountpoint(self):
        self.volume_api.attachment_update(
            self.fake_client, self.fake_attach_id, self.fake_connector,
            mountpoint='fake_mountpoint')
        self.fake_connector['mountpoint'] = 'fake_mountpoint'
        self.fake_client.attachments.update.assert_called_once_with(
            self.fake_attach_id, self.fake_connector)

    def test_attachment_update_client_exception(self):
        self.fake_client.attachments.update.side_effect = (
            cinder_exception.ClientException(code=1))
        self.assertRaises(
            cinder_exception.ClientException,
            self.volume_api.attachment_update,
            self.fake_client, self.fake_attach_id, self.fake_connector)

    def test_attachment_complete(self):
        self.volume_api.attachment_complete(self.fake_client,
                                            self.fake_attach_id)
        self.fake_client.attachments.complete.assert_called_once_with(
            self.fake_attach_id)

    def test_attachment_complete_client_exception(self):
        self.fake_client.attachments.complete.side_effect = (
            cinder_exception.ClientException(code=1))
        self.assertRaises(
            cinder_exception.ClientException,
            self.volume_api.attachment_complete,
            self.fake_client, self.fake_attach_id)

    def test_attachment_delete(self):
        self.volume_api.attachment_delete(self.fake_client,
                                          self.fake_attach_id)
        self.fake_client.attachments.delete.assert_called_once_with(
            self.fake_attach_id)

    def test_attachment_delete_client_exception(self):
        self.fake_client.attachments.delete.side_effect = (
            cinder_exception.ClientException(code=1))
        self.assertRaises(
            cinder_exception.ClientException,
            self.volume_api.attachment_delete,
            self.fake_client, self.fake_attach_id)

    def test_attachment_delete_retries(self):
        # Make delete fail two times and succeed on the third attempt.
        self.fake_client.attachments.delete.side_effect = [
            apiclient_exception.InternalServerError(),
            apiclient_exception.InternalServerError(),
            lambda aid: 'foo']

        # Make sure we get a clean result.
        self.assertIsNone(self.volume_api.attachment_delete(
            self.fake_client, self.fake_attach_id))

        # Assert that we called delete three times due to the retry
        # decorator.
        self.fake_client.attachments.delete.assert_has_calls([
            mock.call(self.fake_attach_id),
            mock.call(self.fake_attach_id),
            mock.call(self.fake_attach_id)])

    def test_api_init_with_no_config(self):
        # Test API initialization without config
        api = cinder_utils.API()
        self.assertEqual(5, api.attachment_create_retry_attempts)

    def test_api_init_with_config_custom_retry_attempts(self):
        # Test API initialization with custom retry attempts
        fake_config = FakeObject(cinder_attachment_retry_attempts=3)
        api = cinder_utils.API(config=fake_config)
        self.assertEqual(3, api.attachment_create_retry_attempts)

    @mock.patch('time.sleep', new=mock.Mock())
    def test_attachment_create_retries_with_custom_attempts(self):
        # Test that custom retry attempts are respected
        fake_config = FakeObject(cinder_attachment_retry_attempts=2)
        volume_api = cinder_utils.API(config=fake_config)

        fake_attach_id = '12345678-1234-5678-1234-567812345678'
        # Make create fail once and succeed on second attempt
        error_msg = self.volume_status_error_msg.format(
            volume_id=self.fake_vol_id, current_status='reserved')
        self.fake_client.attachments.create.side_effect = [
            cinder_exception.BadRequest(400, message=error_msg),
            fake_attach_id]

        # Should succeed on second attempt
        result = volume_api.attachment_create(
            self.fake_client, self.fake_vol_id)

        self.assertEqual(fake_attach_id, result)
        # Should have called twice
        self.assertEqual(2, self.fake_client.attachments.create.call_count)

    @mock.patch('time.sleep', new=mock.Mock())
    def test_attachment_create_max_retries_exceeded(self):
        # Test that retries stop after max attempts
        fake_config = FakeObject(cinder_attachment_retry_attempts=3)
        volume_api = cinder_utils.API(config=fake_config)

        # Make all calls fail with retryable error
        error_msg = self.volume_status_error_msg.format(
            volume_id=self.fake_vol_id, current_status='reserved')
        self.fake_client.attachments.create.side_effect = \
            cinder_exception.BadRequest(400, message=error_msg)

        # Should raise after 3 attempts
        self.assertRaises(
            cinder_exception.BadRequest,
            volume_api.attachment_create,
            self.fake_client, self.fake_vol_id)

        # Should have called 3 times
        self.assertEqual(3, self.fake_client.attachments.create.call_count)

    def test_retry_on_bad_request_with_status_must_be_available_or_downloading(self):  # noqa
        # Test that BadRequest with "status must be available or
        # downloading" is retried
        error = cinder_exception.BadRequest(
            400,
            message='status must be available or downloading to reserve')
        self.assertTrue(cinder_utils._retry_on_bad_request(error))

    def test_retry_on_bad_request_with_status_available_or_in_use_or_downloading(self):  # noqa
        # Test that BadRequest with "status must be" and "to reserve" is
        # retried
        error = cinder_exception.BadRequest(
            400,
            message='Volume status must be available or in-use or '
                    'downloading to reserve')
        self.assertTrue(cinder_utils._retry_on_bad_request(error))

    def test_retry_on_bad_request_with_other_error(self):
        # Test that BadRequest with other errors is not retried
        error = cinder_exception.BadRequest(
            400, message='Invalid input received')
        self.assertFalse(cinder_utils._retry_on_bad_request(error))

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch('glance_store.common.cinder_utils.LOG')
    def test_attachment_create_retry_logging(self, mock_log):
        # Test that retry attempts are logged correctly
        fake_attach_id = '12345678-1234-5678-1234-567812345678'
        # Make create fail twice with status error and succeed on third
        error_msg = self.volume_status_error_msg.format(
            volume_id=self.fake_vol_id, current_status='reserved')
        self.fake_client.attachments.create.side_effect = [
            cinder_exception.BadRequest(400, message=error_msg),
            cinder_exception.BadRequest(400, message=error_msg),
            fake_attach_id]

        result = self.volume_api.attachment_create(
            self.fake_client, self.fake_vol_id)

        self.assertEqual(fake_attach_id, result)

        # Check that debug logs were called for retries
        self.assertEqual(2, mock_log.debug.call_count)

        # Check first retry log (should extract 'reserved' state)
        first_call = mock_log.debug.call_args_list[0]
        self.assertIn('Attachment create failed for volume',
                      first_call[0][0])
        # The second positional argument contains the dict
        log_dict = first_call[0][1]
        self.assertIn('state', log_dict)
        self.assertEqual('reserved', log_dict['state'])

    @mock.patch('time.sleep', new=mock.Mock())
    @mock.patch('glance_store.common.cinder_utils.LOG')
    def test_attachment_create_retry_volume_state_extraction(self, mock_log):
        # Test volume state extraction from error message
        fake_attach_id = '12345678-1234-5678-1234-567812345678'
        error_msg = self.volume_status_error_msg.format(
            volume_id=self.fake_vol_id, current_status='in-use')
        self.fake_client.attachments.create.side_effect = [
            cinder_exception.BadRequest(400, message=error_msg),
            fake_attach_id]

        result = self.volume_api.attachment_create(
            self.fake_client, self.fake_vol_id)

        self.assertEqual(fake_attach_id, result)

        # Verify debug log was called
        mock_log.debug.assert_called_once()
        # The second positional argument contains the dict
        log_dict = mock_log.debug.call_args[0][1]

        # Should have extracted 'in-use' as the volume state
        self.assertEqual('in-use', log_dict['state'])

    @mock.patch('time.sleep', new=mock.Mock())
    def test_attachment_create_no_retry_on_non_status_bad_request(self):
        # Test that BadRequest errors not related to status are not retried
        self.fake_client.attachments.create.side_effect = \
            cinder_exception.BadRequest(400,
                                        message='Invalid connector format')

        # Should raise immediately without retries
        self.assertRaises(
            cinder_exception.BadRequest,
            self.volume_api.attachment_create,
            self.fake_client, self.fake_vol_id)

        # Should have only called once (no retries)
        self.assertEqual(1,
                         self.fake_client.attachments.create.call_count)
