# Copyright (c) 2025 Thomas Goirand <zigo@debian.org>
#
# 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
from flask import Flask, request
import json

from vmms.tests import base
from vmms.policy import enforcer


class TestKeystoneTokenValidation(base.VMMSTestCase):
    """Test Keystone token validation and role checking."""

    def setUp(self):
        super(TestKeystoneTokenValidation, self).setUp()

        # Create a minimal Flask app for testing
        self.test_app = Flask(__name__)
        self.test_app.config['TESTING'] = True

        # Clear the token cache before each test to ensure isolation
        enforcer._token_cache.clear()

    @mock.patch('keystoneauth1.session.Session.get')
    def test_validate_keystone_token_success(self, mock_session_get):
        """Test successful token validation returns correct roles."""
        # Mock successful response from Keystone
        mock_response = mock.Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            'token': {
                'roles': [
                    {'name': 'admin'},
                    {'name': 'member'}
                ]
            }
        }
        mock_session_get.return_value = mock_response

        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Test token validation
        token = 'valid-token-123'
        roles = enforcer._validate_keystone_token(mock_conf, token)

        # Assertions
        self.assertEqual(roles, ['admin', 'member'])
        mock_session_get.assert_called_once_with(
            'http://keystone.example.com/v3/auth/tokens',
            headers={'X-Auth-Token': token},
            authenticated=False,
            raise_exc=True
        )

    @mock.patch('keystoneauth1.session.Session.get')
    def test_validate_keystone_token_invalid_token(self, mock_session_get):
        """Test that invalid token returns empty list."""
        # Mock failed response from Keystone
        mock_response = mock.Mock()
        mock_response.status_code = 401
        mock_session_get.side_effect = Exception("Token validation failed")

        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Test token validation
        token = 'invalid-token-123'
        roles = enforcer._validate_keystone_token(mock_conf, token)

        # Assertions - should return empty list, not False
        self.assertEqual(roles, [])
        self.assertEqual(len(enforcer._token_cache), 0)

    @mock.patch('keystoneauth1.session.Session.get')
    def test_validate_keystone_token_empty_token(self, mock_session_get):
        """Test that empty token returns False."""
        # Create mock config
        mock_conf = mock.Mock()

        # Test token validation
        roles = enforcer._validate_keystone_token(mock_conf, None)

        # Assertions
        self.assertFalse(roles)  # Should return False for None/empty token
        mock_session_get.assert_not_called()

    @mock.patch('keystoneauth1.session.Session.get')
    def test_validate_keystone_token_cache_hit(self, mock_session_get):
        """Test that token validation uses cache when available."""
        # Add token to cache manually
        token = 'cached-token-123'
        enforcer._token_cache[token] = {
            'expires': enforcer.time.time() + 100,  # Expires in future
            'roles': ['cached_role']
        }

        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Test token validation
        roles = enforcer._validate_keystone_token(mock_conf, token)

        # Assertions
        self.assertEqual(roles, ['cached_role'])
        mock_session_get.assert_not_called()

    @mock.patch('keystoneauth1.session.Session.get')
    def test_validate_keystone_token_cache_expired(self, mock_session_get):
        """Test that expired cache entry triggers new validation."""
        # Add expired token to cache manually
        token = 'expired-token-123'
        enforcer._token_cache[token] = {
            'expires': enforcer.time.time() - 100,  # Expired
            'roles': ['old_role']
        }

        # Mock successful response from Keystone
        mock_response = mock.Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            'token': {
                'roles': [{'name': 'new_role'}]
            }
        }
        mock_session_get.return_value = mock_response

        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Test token validation
        roles = enforcer._validate_keystone_token(mock_conf, token)

        # Assertions
        self.assertEqual(roles, ['new_role'])
        mock_session_get.assert_called_once()

    def test_enforce_policy_invalid_token_returns_401(self):
        """Test that invalid token results in 401 response."""
        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Mock request with invalid token
        with self.test_app.test_request_context(
            '/v2/vms',
            headers={
                'X-Auth-Token': 'invalid-token',
                'X-Identity-Status': 'Confirmed',
                'X-User-Id': 'test-user',
                'X-Project-Id': 'test-project',
                'X-Roles': 'admin'
            }
        ):
            # Mock token validation to fail
            with mock.patch('vmms.policy.enforcer._validate_keystone_token') as mock_validate:
                mock_validate.return_value = []

                # Call enforce_policy
                result = enforcer.enforce_policy(mock_conf, 'vmms:list')

                # Should return JSON response with 401 status
                self.assertEqual(result[1], 401)
                self.assertIn('error', result[0].json)
                self.assertEqual(result[0].json['error'], 'Invalid or expired Keystone token')

    def test_enforce_policy_role_mismatch_returns_403(self):
        """Test that role mismatch between headers and Keystone results in 403."""
        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Mock request with role mismatch
        with self.test_app.test_request_context(
            '/v2/vms',
            headers={
                'X-Auth-Token': 'valid-token',
                'X-Identity-Status': 'Confirmed',
                'X-User-Id': 'test-user',
                'X-Project-Id': 'test-project',
                'X-Roles': 'admin,user'  # Header says admin,user
            }
        ):
            # Mock token validation to return different roles than headers
            with mock.patch('vmms.policy.enforcer._validate_keystone_token') as mock_validate:
                mock_validate.return_value = ['member']  # Keystone says member

                # Call enforce_policy
                result = enforcer.enforce_policy(mock_conf, 'vmms:list')

                # Should return JSON response with 403 status
                self.assertEqual(result[1], 403)
                self.assertIn('error', result[0].json)
                self.assertEqual(result[0].json['error'], 'Forbidden: Role mismatch')

    def test_enforce_policy_role_match_succeeds(self):
        """Test that matching roles between headers and Keystone succeeds."""
        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Mock request with matching roles
        with self.test_app.test_request_context(
            '/v2/vms',
            headers={
                'X-Auth-Token': 'valid-token',
                'X-Identity-Status': 'Confirmed',
                'X-User-Id': 'test-user',
                'X-Project-Id': 'test-project',
                'X-Roles': 'admin,member'  # Header says admin,member
            }
        ):
            # Mock token validation to return same roles as headers
            with mock.patch('vmms.policy.enforcer._validate_keystone_token') as mock_validate:
                mock_validate.return_value = ['admin', 'member']  # Keystone also says admin,member

                # Mock enforcer initialization to avoid file access
                with mock.patch('vmms.policy.enforcer.init_enforcer') as mock_init:
                    mock_enforcer = mock.Mock()
                    mock_enforcer.enforce.return_value = True
                    mock_init.return_value = mock_enforcer

                    # Call enforce_policy
                    result = enforcer.enforce_policy(mock_conf, 'vmms:list')

                    # Should return True (success)
                    self.assertTrue(result)

    def test_enforce_policy_no_roles_in_token_fails(self):
        """Test that no roles in token results in 401."""
        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Mock request
        with self.test_app.test_request_context(
            '/v2/vms',
            headers={
                'X-Auth-Token': 'valid-token',
                'X-Identity-Status': 'Confirmed',
                'X-User-Id': 'test-user',
                'X-Project-Id': 'test-project',
                'X-Roles': 'admin'
            }
        ):
            # Mock token validation to return no roles
            with mock.patch('vmms.policy.enforcer._validate_keystone_token') as mock_validate:
                mock_validate.return_value = []  # No roles

                # Call enforce_policy
                result = enforcer.enforce_policy(mock_conf, 'vmms:list')

                # Should return JSON response with 401 status
                self.assertEqual(result[1], 401)
                self.assertIn('error', result[0].json)
                self.assertEqual(result[0].json['error'], 'Invalid or expired Keystone token')

    @mock.patch('keystoneauth1.session.Session.get')
    def test_validate_keystone_token_with_401_response(self, mock_session_get):
        """Test that 401 response from Keystone is handled properly."""
        # Mock 401 response from Keystone
        mock_response = mock.Mock()
        mock_response.status_code = 401
        mock_session_get.return_value = mock_response

        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Test token validation
        token = 'unauthorized-token-123'
        roles = enforcer._validate_keystone_token(mock_conf, token)

        # Should return empty list, not False
        self.assertEqual(roles, [])
        mock_session_get.assert_called_once()

    @mock.patch('keystoneauth1.session.Session.get')
    def test_validate_keystone_token_with_connection_error(self, mock_session_get):
        """Test that connection errors during token validation are handled."""
        # Mock connection error
        mock_session_get.side_effect = ConnectionError("Connection failed")

        # Create mock config
        mock_conf = mock.Mock()
        mock_conf.identity.auth_url = 'http://keystone.example.com'

        # Test token validation
        token = 'connection-error-token-123'
        roles = enforcer._validate_keystone_token(mock_conf, token)

        # Should return empty list, not False
        self.assertEqual(roles, [])
        mock_session_get.assert_called_once()


class TestPolicyDecoratorWithTokenValidation(base.VMMSTestCase):
    """Test policy decorator with token validation scenarios."""

    def setUp(self):
        super(TestPolicyDecoratorWithTokenValidation, self).setUp()

        # Create a minimal Flask app for testing
        self.test_app = Flask(__name__)
        self.test_app.config['TESTING'] = True

        # Clear the token cache before each test to ensure isolation
        enforcer._token_cache.clear()

        # Create a test route for the decorator
        @enforcer.require_policy_factory(lambda: mock.Mock())('vmms:list')
        def test_route():
            return {'result': 'success'}, 200

        self.test_route = test_route

    @mock.patch('vmms.policy.enforcer._validate_keystone_token')
    def test_decorator_with_invalid_token(self, mock_validate_token):
        """Test that decorator returns 401 for invalid token."""
        mock_validate_token.return_value = []

        with self.test_app.test_request_context(
            '/test',
            headers={
                'X-Identity-Status': 'Confirmed',
                'X-User-Id': 'test-user',
                'X-Project-Id': 'test-project',
                'X-Roles': 'admin',
                'X-Auth-Token': 'invalid-token'
            }
        ):
            # Since we're testing the internal logic, we'll call the policy check directly
            # This simulates what happens inside the decorator
            mock_conf = mock.Mock()
            result = enforcer.enforce_policy(mock_conf, 'vmms:list')

            self.assertEqual(result[1], 401)
            self.assertEqual(result[0].json['error'], 'Invalid or expired Keystone token')

    @mock.patch('vmms.policy.enforcer._validate_keystone_token')
    def test_decorator_with_role_mismatch(self, mock_validate_token):
        """Test that decorator returns 403 for role mismatch."""
        # Simulate header roles vs Keystone roles mismatch
        mock_validate_token.return_value = ['member']  # Keystone says member

        with self.test_app.test_request_context(
            '/test',
            headers={
                'X-Identity-Status': 'Confirmed',
                'X-User-Id': 'test-user',
                'X-Project-Id': 'test-project',
                'X-Roles': 'admin',  # Headers say admin
                'X-Auth-Token': 'valid-token'
            }
        ):
            mock_conf = mock.Mock()
            result = enforcer.enforce_policy(mock_conf, 'vmms:list')

            self.assertEqual(result[1], 403)
            self.assertEqual(result[0].json['error'], 'Forbidden: Role mismatch')

    @mock.patch('vmms.policy.enforcer._validate_keystone_token')
    def test_decorator_with_valid_token_and_matching_roles(self, mock_validate_token):
        """Test that decorator succeeds with valid token and matching roles."""
        # Simulate matching roles
        mock_validate_token.return_value = ['admin']  # Keystone says admin

        with self.test_app.test_request_context(
            '/test',
            headers={
                'X-Identity-Status': 'Confirmed',
                'X-User-Id': 'test-user',
                'X-Project-Id': 'test-project',
                'X-Roles': 'admin',  # Headers say admin
                'X-Auth-Token': 'valid-token'
            }
        ):
            # Mock enforcer to succeed
            with mock.patch('vmms.policy.enforcer.init_enforcer') as mock_init:
                mock_enforcer = mock.Mock()
                mock_enforcer.enforce.return_value = True
                mock_init.return_value = mock_enforcer

                mock_conf = mock.Mock()
                result = enforcer.enforce_policy(mock_conf, 'vmms:list')

                # Should return True (success)
                self.assertTrue(result)
