"""
Tests for configuration CLI commands.

Tests the new configuration management CLI commands:
- config-show
- config-validate
- config-troubleshoot
- config-files
"""

import os
import sys
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock, mock_open
from io import StringIO

# Add the project root to the path
sys.path.insert(0, str(Path(__file__).parent.parent))

from cli.commands.config import ConfigCommands
from cli.presenters.config import ConfigPresenter
from config import MarkitectConfig, ConfigurationError


class TestConfigCommands:
    """Test suite for configuration CLI commands."""

    def setup_method(self):
        """Set up test fixtures."""
        self.config_commands = ConfigCommands()

    def _get_mock_config(self):
        """Get a mock configuration for testing."""
        return MarkitectConfig(
            gitea_url="https://github.com",
            repo_owner="test_owner",
            repo_name="test_repo",
            workspace_dir=Path(".test_workspace"),
            database_path=Path("/tmp/test.db")
        )

    def _get_mock_status(self):
        """Get mock configuration status."""
        return {
            'sources': {
                'environment': {'loaded': True, 'path': 'Environment'},
                'env_file': {'loaded': True, 'path': '.env.tddai'},
                'defaults': {'loaded': True, 'path': 'System'}
            }
        }

    @patch('cli.commands.config.get_unified_config')
    @patch('cli.commands.config.get_config_status')
    @patch('sys.stdout', new_callable=StringIO)
    def test_config_show_command_displays_current_configuration_status(self, mock_stdout, mock_status, mock_config):
        """Test config-show command displays current configuration status."""
        mock_config.return_value = self._get_mock_config()
        mock_status.return_value = self._get_mock_status()

        self.config_commands.show_config()

        output = mock_stdout.getvalue()
        assert "🔧 Configuration Status" in output
        assert "Core Configuration" in output
        # The output shows real config is being used, verify mock was called
        mock_config.assert_called_once()
        mock_status.assert_called_once()

    @patch('cli.commands.config.get_unified_config')
    @patch('cli.commands.config.get_config_status')
    @patch('os.getenv')
    @patch('sys.stdout', new_callable=StringIO)
    def test_show_config_with_sensitive(self, mock_stdout, mock_getenv, mock_status, mock_config):
        """Test config-show with sensitive information."""
        mock_config.return_value = self._get_mock_config()
        mock_status.return_value = self._get_mock_status()
        mock_getenv.side_effect = lambda key, default=None: "test_token_12345678" if "TOKEN" in key else default

        self.config_commands.show_config(show_sensitive=True)

        output = mock_stdout.getvalue()
        assert "🔧 Configuration Status" in output
        # Should show some masked token (pattern varies)
        assert "..." in output and "tok" in output

    @patch('cli.commands.config.get_unified_config')
    @patch('sys.stderr', new_callable=StringIO)
    def test_show_config_error(self, mock_stderr, mock_config):
        """Test config-show with configuration error."""
        mock_config.side_effect = ConfigurationError("Test configuration error")

        with pytest.raises(SystemExit) as exc_info:
            self.config_commands.show_config()

        assert exc_info.value.code == 1

    @patch('cli.commands.config.get_unified_config')
    @patch('sys.stdout', new_callable=StringIO)
    def test_config_validate_command_reports_valid_configuration_status(self, mock_stdout, mock_config):
        """Test config-validate command reports valid configuration status."""
        mock_config.return_value = self._get_mock_config()

        with patch.object(self.config_commands, '_perform_validation_checks') as mock_validate:
            mock_validate.return_value = [
                {'check': 'Test check', 'status': 'success', 'message': 'All good'},
                {'check': 'Another check', 'status': 'success', 'message': 'Perfect'}
            ]

            self.config_commands.validate_config()

            output = mock_stdout.getvalue()
            assert "✅ Configuration Validation" in output
            assert "2/2 checks passed" in output
            assert "✅ Test check" in output

    @patch('cli.commands.config.get_unified_config')
    @patch('sys.stdout', new_callable=StringIO)
    def test_validate_config_with_errors(self, mock_stdout, mock_config):
        """Test config validation with errors."""
        mock_config.return_value = self._get_mock_config()

        with patch.object(self.config_commands, '_perform_validation_checks') as mock_validate:
            mock_validate.return_value = [
                {'check': 'Good check', 'status': 'success', 'message': 'All good'},
                {'check': 'Bad check', 'status': 'error', 'message': 'Error found', 'suggestion': 'Fix it'}
            ]

            with pytest.raises(SystemExit) as exc_info:
                self.config_commands.validate_config()

            assert exc_info.value.code == 1
            output = mock_stdout.getvalue()
            assert "❌ 1 errors" in output

    @patch('cli.commands.config.get_unified_config')
    @patch('cli.commands.config.get_config_status')
    @patch('sys.stdout', new_callable=StringIO)
    def test_config_troubleshoot_command_provides_diagnostic_information(self, mock_stdout, mock_status, mock_config):
        """Test config-troubleshoot command provides diagnostic information."""
        mock_config.return_value = self._get_mock_config()
        mock_status.return_value = self._get_mock_status()

        with patch.object(self.config_commands, '_run_diagnostics') as mock_diagnostics:
            mock_diagnostics.return_value = {
                'environment': {'python_version': '3.8.0', 'environment_variables': {}},
                'filesystem': {},
                'config_files': {},
                'git_repository': {},
                'network': {}
            }

            self.config_commands.troubleshoot_config()

            output = mock_stdout.getvalue()
            assert "🔍 Configuration Troubleshooting" in output
            assert "✅ Configuration loaded successfully" in output

    @patch('cli.commands.config.get_unified_config')
    @patch('sys.stdout', new_callable=StringIO)
    def test_troubleshoot_config_failure(self, mock_stdout, mock_config):
        """Test config troubleshooting when config loading fails."""
        mock_config.side_effect = ConfigurationError("Failed to load config")

        with patch.object(self.config_commands, '_run_basic_diagnostics') as mock_diagnostics:
            mock_diagnostics.return_value = {
                'environment': {
                    'python_version': '3.8.0',
                    'python_executable': '/usr/bin/python3',
                    'current_directory': '/test',
                    'environment_variables': {}
                },
                'filesystem': {},
                'config_files': {},
                'git_repository': {'is_git_repository': False}
            }

            self.config_commands.troubleshoot_config()

            output = mock_stdout.getvalue()
            assert "🔍 Configuration Troubleshooting" in output
            # Should not show "Configuration loaded successfully"
            assert "✅ Configuration loaded successfully" not in output

    @patch('sys.stdout', new_callable=StringIO)
    def test_check_config_files(self, mock_stdout):
        """Test config files checking."""
        with patch.object(self.config_commands, '_check_configuration_files') as mock_check:
            mock_check.return_value = {
                '.env.tddai': {
                    'path': '.env.tddai',
                    'exists': True,
                    'readable': True,
                    'size': 100,
                    'modified': 1234567890,
                    'parsed_variables': 3,
                    'parse_error': None
                },
                '.env': {
                    'path': '.env',
                    'exists': False,
                    'readable': False,
                    'size': 0,
                    'modified': None
                }
            }

            self.config_commands.check_config_files()

            output = mock_stdout.getvalue()
            assert "📁 Configuration Files Status" in output
            assert "✅ .env.tddai" in output
            assert "❌ .env" in output

    def test_perform_validation_checks_all_valid(self):
        """Test validation checks with all valid configuration."""
        config = self._get_mock_config()

        with patch.dict('os.environ', {'GITEA_API_TOKEN': 'test_token'}):
            results = self.config_commands._perform_validation_checks(config)

        # Should have checks for required fields, URL format, workspace, and auth token
        assert len(results) == 6

        # All should be successful
        success_results = [r for r in results if r['status'] == 'success']
        assert len(success_results) == 6

    def test_perform_validation_checks_missing_fields(self):
        """Test validation checks with missing required fields."""
        # Create a config that bypasses normal validation
        config = MarkitectConfig.__new__(MarkitectConfig)
        config.gitea_url = ""
        config.repo_owner = ""
        config.repo_name = "test_repo"
        config.workspace_dir = Path(".test_workspace")

        results = self.config_commands._perform_validation_checks(config)

        # Should have error results for missing fields
        error_results = [r for r in results if r['status'] == 'error']
        assert len(error_results) >= 2  # At least gitea_url and repo_owner

    def test_perform_validation_checks_invalid_gitea_url(self):
        """Test validation checks with invalid Gitea URL format."""
        # Create a config that bypasses normal validation
        config = MarkitectConfig.__new__(MarkitectConfig)
        config.gitea_url = "invalid-url"
        config.repo_owner = "test_owner"
        config.repo_name = "test_repo"
        config.workspace_dir = Path(".test_workspace")

        results = self.config_commands._perform_validation_checks(config)

        # Should have error for invalid URL format
        url_errors = [r for r in results if 'URL format' in r['check'] and r['status'] == 'error']
        assert len(url_errors) == 1

    @patch('os.access')
    def test_check_filesystem_permissions(self, mock_access):
        """Test filesystem diagnostics."""
        mock_access.return_value = True

        result = self.config_commands._check_filesystem()

        assert 'current_directory' in result
        assert 'home_directory' in result
        assert result['current_directory']['readable'] is True
        assert result['current_directory']['writable'] is True

    @patch('subprocess.run')
    def test_check_git_repository_with_git(self, mock_run):
        """Test git repository checking with git available."""
        # Mock git commands
        def side_effect(cmd, **kwargs):
            if 'remote' in cmd:
                return MagicMock(returncode=0, stdout="https://github.com/test/repo.git")
            elif 'branch' in cmd:
                return MagicMock(returncode=0, stdout="main")
            return MagicMock(returncode=0, stdout="")

        mock_run.side_effect = side_effect

        with patch('pathlib.Path.exists', return_value=True):
            result = self.config_commands._check_git_repository()

        assert result['is_git_repository'] is True
        assert 'remote_origin' in result
        assert 'current_branch' in result

    def test_check_git_repository_without_git(self):
        """Test git repository checking without git directory."""
        with patch('pathlib.Path.exists', return_value=False):
            result = self.config_commands._check_git_repository()

        assert result['is_git_repository'] is False

    @patch('urllib.request.urlopen')
    def test_check_network_connectivity_success(self, mock_urlopen):
        """Test successful network connectivity check."""
        mock_response = MagicMock()
        mock_response.getcode.return_value = 200
        mock_urlopen.return_value.__enter__.return_value = mock_response

        config = self._get_mock_config()
        result = self.config_commands._check_network_connectivity(config)

        assert 'gitea_connectivity' in result
        assert result['gitea_connectivity']['reachable'] is True
        assert result['gitea_connectivity']['status_code'] == 200

    @patch('urllib.request.urlopen')
    def test_check_network_connectivity_failure(self, mock_urlopen):
        """Test failed network connectivity check."""
        mock_urlopen.side_effect = Exception("Connection failed")

        config = self._get_mock_config()
        result = self.config_commands._check_network_connectivity(config)

        assert 'gitea_connectivity' in result
        assert result['gitea_connectivity']['reachable'] is False
        assert 'error' in result['gitea_connectivity']

    @patch('pathlib.Path.exists')
    @patch('os.access')
    def test_check_configuration_files_existing(self, mock_access, mock_exists):
        """Test configuration file checking with existing files."""
        mock_exists.return_value = True
        mock_access.return_value = True

        with patch('pathlib.Path.stat') as mock_stat:
            mock_stat.return_value.st_size = 100
            mock_stat.return_value.st_mtime = 1234567890

            with patch('config.load_env_file', return_value={'TEST': 'value'}):
                result = self.config_commands._check_configuration_files()

        assert '.env.tddai' in result
        assert result['.env.tddai']['exists'] is True
        assert result['.env.tddai']['readable'] is True
        assert result['.env.tddai']['size'] == 100

    def test_run_diagnostics_complete(self):
        """Test running complete diagnostics."""
        config = self._get_mock_config()

        with patch.object(self.config_commands, '_check_environment') as mock_env, \
             patch.object(self.config_commands, '_check_filesystem') as mock_fs, \
             patch.object(self.config_commands, '_check_configuration_files') as mock_files, \
             patch.object(self.config_commands, '_check_git_repository') as mock_git, \
             patch.object(self.config_commands, '_check_network_connectivity') as mock_network:

            mock_env.return_value = {}
            mock_fs.return_value = {}
            mock_files.return_value = {}
            mock_git.return_value = {}
            mock_network.return_value = {}

            result = self.config_commands._run_diagnostics(config)

            assert 'environment' in result
            assert 'filesystem' in result
            assert 'config_files' in result
            assert 'git_repository' in result
            assert 'network' in result

    def test_run_basic_diagnostics(self):
        """Test running basic diagnostics when config fails."""
        with patch.object(self.config_commands, '_check_environment') as mock_env, \
             patch.object(self.config_commands, '_check_filesystem') as mock_fs, \
             patch.object(self.config_commands, '_check_configuration_files') as mock_files, \
             patch.object(self.config_commands, '_check_git_repository') as mock_git:

            mock_env.return_value = {}
            mock_fs.return_value = {}
            mock_files.return_value = {}
            mock_git.return_value = {}

            result = self.config_commands._run_basic_diagnostics()

            assert 'environment' in result
            assert 'filesystem' in result
            assert 'config_files' in result
            assert 'git_repository' in result
            assert 'network' not in result  # Should not include network check


class TestConfigPresenter:
    """Test suite for configuration presenter."""

    def setup_method(self):
        """Set up test fixtures."""
        self.presenter = ConfigPresenter()

    @patch('sys.stdout', new_callable=StringIO)
    def test_show_error(self, mock_stdout):
        """Test error display."""
        self.presenter.show_error("Test error message")

        output = mock_stdout.getvalue()
        assert "❌ Test error message" in output

    @patch('sys.stdout', new_callable=StringIO)
    @patch('pathlib.Path.exists')
    @patch('pathlib.Path.iterdir')
    def test_show_gitea_configuration(self, mock_iterdir, mock_exists, mock_stdout):
        """Test Gitea configuration display."""
        mock_exists.return_value = False  # Don't check real filesystem
        mock_iterdir.return_value = []

        config = MarkitectConfig(
            gitea_url="https://github.com",
            repo_owner="test_owner",
            repo_name="test_repo",
            workspace_dir=Path(".test_workspace")
        )
        status = {'sources': {}}

        self.presenter.show_configuration(config, status, show_sensitive=False)

        output = mock_stdout.getvalue()
        assert "🔧 Configuration Status" in output
        assert "Core Configuration" in output
        assert "https://github.com" in output

    @patch('sys.stdout', new_callable=StringIO)
    def test_show_validation_results_success(self, mock_stdout):
        """Test validation results display with all success."""
        results = [
            {'check': 'Test 1', 'status': 'success', 'message': 'Good'},
            {'check': 'Test 2', 'status': 'success', 'message': 'Also good'}
        ]

        self.presenter.show_validation_results(results)

        output = mock_stdout.getvalue()
        assert "✅ Configuration Validation" in output
        assert "2/2 checks passed" in output
        assert "✅ Test 1" in output
        assert "✅ Test 2" in output

    @patch('sys.stdout', new_callable=StringIO)
    def test_show_validation_results_with_errors(self, mock_stdout):
        """Test validation results display with errors."""
        results = [
            {'check': 'Good test', 'status': 'success', 'message': 'Good'},
            {'check': 'Bad test', 'status': 'error', 'message': 'Bad', 'suggestion': 'Fix it'},
            {'check': 'Warning test', 'status': 'warning', 'message': 'Warning', 'suggestion': 'Consider this'}
        ]

        self.presenter.show_validation_results(results)

        output = mock_stdout.getvalue()
        assert "1/3 checks passed" in output
        assert "⚠️  1 warnings" in output
        assert "❌ 1 errors" in output
        assert "💡 Fix it" in output
        assert "💡 Consider this" in output

    @patch('sys.stdout', new_callable=StringIO)
    def test_show_config_file_status(self, mock_stdout):
        """Test configuration file status display."""
        file_checks = {
            '.env.tddai': {
                'path': '.env.tddai',
                'exists': True,
                'readable': True,
                'size': 100,
                'modified': 1234567890,
                'parsed_variables': 3,
                'parse_error': None
            },
            '.env': {
                'path': '.env',
                'exists': False,
                'readable': False,
                'size': 0,
                'modified': None
            }
        }

        self.presenter.show_config_file_status(file_checks)

        output = mock_stdout.getvalue()
        assert "📁 Configuration Files Status" in output
        assert "✅ .env.tddai" in output
        assert "❌ .env" in output
        assert "🔧 Variables: 3" in output


