#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Unit tests for the Code Analyzer service
"""

import unittest
from unittest.mock import patch, MagicMock, mock_open
import os
import sys
import json
from pathlib import Path

# Add the project root directory to the Python path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))

from src.services.code_analyzer import CodeAnalyzer


class TestCodeAnalyzer(unittest.TestCase):
    """Test cases for the CodeAnalyzer class"""

    def setUp(self):
        """Set up test fixtures"""
        self.analyzer = CodeAnalyzer()
        self.test_repo_path = "/test/repo"
    
    @patch('os.path.exists')
    @patch('subprocess.run')
    def test_analyze_python_code(self, mock_run, mock_exists):
        """Test analyze_python_code method"""
        # Set up the mocks
        mock_exists.return_value = True
        
        # Mock the subprocess.run result
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = json.dumps({
            "messages": [
                {
                    "type": "convention",
                    "module": "test_module",
                    "obj": "",
                    "line": 10,
                    "column": 0,
                    "path": "test.py",
                    "symbol": "missing-docstring",
                    "message": "Missing module docstring",
                    "message-id": "C0111"
                }
            ]
        })
        mock_run.return_value = mock_process
        
        # Mock the file discovery
        with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.py']):
            # Call the method
            result = self.analyzer.analyze_python_code(self.test_repo_path)
            
            # Verify the result
            self.assertEqual(len(result['issues']), 1)
            self.assertEqual(result['issue_count'], 1)
            self.assertEqual(result['issues'][0]['type'], 'convention')
            self.assertEqual(result['issues'][0]['file'], 'test.py')
            self.assertEqual(result['issues'][0]['line'], 10)
            self.assertEqual(result['issues'][0]['message'], 'Missing module docstring')
    
    @patch('os.path.exists')
    @patch('subprocess.run')
    def test_analyze_javascript_code(self, mock_run, mock_exists):
        """Test analyze_javascript_code method"""
        # Set up the mocks
        mock_exists.return_value = True
        
        # Mock the subprocess.run result
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = json.dumps([
            {
                "filePath": "/test/repo/test.js",
                "messages": [
                    {
                        "ruleId": "semi",
                        "severity": 2,
                        "message": "Missing semicolon.",
                        "line": 5,
                        "column": 20,
                        "nodeType": "ExpressionStatement"
                    }
                ],
                "errorCount": 1,
                "warningCount": 0,
                "fixableErrorCount": 1,
                "fixableWarningCount": 0
            }
        ])
        mock_run.return_value = mock_process
        
        # Mock the file discovery
        with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.js']):
            # Call the method
            result = self.analyzer.analyze_javascript_code(self.test_repo_path)
            
            # Verify the result
            self.assertEqual(len(result['issues']), 1)
            self.assertEqual(result['issue_count'], 1)
            self.assertEqual(result['issues'][0]['type'], 'error')
            self.assertEqual(result['issues'][0]['file'], 'test.js')
            self.assertEqual(result['issues'][0]['line'], 5)
            self.assertEqual(result['issues'][0]['message'], 'Missing semicolon.')
    
    @patch('os.path.exists')
    @patch('subprocess.run')
    def test_analyze_typescript_code(self, mock_run, mock_exists):
        """Test analyze_typescript_code method"""
        # Set up the mocks
        mock_exists.return_value = True
        
        # Mock the subprocess.run results
        # First for ESLint
        eslint_process = MagicMock()
        eslint_process.returncode = 0
        eslint_process.stdout = json.dumps([
            {
                "filePath": "/test/repo/test.ts",
                "messages": [
                    {
                        "ruleId": "@typescript-eslint/no-unused-vars",
                        "severity": 1,
                        "message": "'x' is defined but never used.",
                        "line": 3,
                        "column": 7,
                        "nodeType": "Identifier"
                    }
                ],
                "errorCount": 0,
                "warningCount": 1,
                "fixableErrorCount": 0,
                "fixableWarningCount": 0
            }
        ])
        
        # Then for TSC
        tsc_process = MagicMock()
        tsc_process.returncode = 2  # Error code for TypeScript compiler
        tsc_process.stderr = "test.ts(10,15): error TS2339: Property 'foo' does not exist on type 'Bar'."
        
        # Set up the mock to return different values on consecutive calls
        mock_run.side_effect = [eslint_process, tsc_process]
        
        # Mock the file discovery
        with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/test.ts']):
            # Call the method
            result = self.analyzer.analyze_typescript_code(self.test_repo_path)
            
            # Verify the result
            self.assertEqual(len(result['issues']), 2)  # One from ESLint, one from TSC
            self.assertEqual(result['issue_count'], 2)
            
            # Check the ESLint issue
            eslint_issue = next(issue for issue in result['issues'] if issue['source'] == 'eslint')
            self.assertEqual(eslint_issue['type'], 'warning')
            self.assertEqual(eslint_issue['file'], 'test.ts')
            self.assertEqual(eslint_issue['line'], 3)
            self.assertEqual(eslint_issue['message'], "'x' is defined but never used.")
            
            # Check the TSC issue
            tsc_issue = next(issue for issue in result['issues'] if issue['source'] == 'tsc')
            self.assertEqual(tsc_issue['type'], 'error')
            self.assertEqual(tsc_issue['file'], 'test.ts')
            self.assertEqual(tsc_issue['line'], 10)
            self.assertEqual(tsc_issue['message'], "Property 'foo' does not exist on type 'Bar'.")
    
    @patch('os.path.exists')
    @patch('subprocess.run')
    def test_analyze_java_code(self, mock_run, mock_exists):
        """Test analyze_java_code method"""
        # Set up the mocks
        mock_exists.return_value = True
        
        # Mock the subprocess.run result
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = """
<?xml version="1.0" encoding="UTF-8"?>
<pmd version="6.55.0" timestamp="2023-06-01T12:00:00.000">
<file name="/test/repo/Test.java">
<violation beginline="10" endline="10" begincolumn="5" endcolumn="20" rule="UnusedLocalVariable" ruleset="Best Practices" class="Test" method="main" variable="unusedVar" externalInfoUrl="https://pmd.github.io/pmd-6.55.0/pmd_rules_java_bestpractices.html#unusedlocalvariable" priority="3">
Avoid unused local variables such as 'unusedVar'.
</violation>
</file>
</pmd>
        """
        mock_run.return_value = mock_process
        
        # Mock the file discovery
        with patch.object(self.analyzer, '_find_files', return_value=['/test/repo/Test.java']):
            # Call the method
            result = self.analyzer.analyze_java_code(self.test_repo_path)
            
            # Verify the result
            self.assertEqual(len(result['issues']), 1)
            self.assertEqual(result['issue_count'], 1)
            self.assertEqual(result['issues'][0]['type'], 'warning')  # Priority 3 maps to warning
            self.assertEqual(result['issues'][0]['file'], 'Test.java')
            self.assertEqual(result['issues'][0]['line'], 10)
            self.assertEqual(result['issues'][0]['message'], "Avoid unused local variables such as 'unusedVar'.")
    
    @patch('os.path.exists')
    @patch('subprocess.run')
    def test_analyze_go_code(self, mock_run, mock_exists):
        """Test analyze_go_code method"""
        # Set up the mocks
        mock_exists.return_value = True
        
        # Mock the subprocess.run result
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = json.dumps({
            "Issues": [
                {
                    "FromLinter": "gosimple",
                    "Text": "S1000: should use a simple channel send/receive instead of select with a single case",
                    "Pos": {
                        "Filename": "test.go",
                        "Line": 15,
                        "Column": 2
                    },
                    "Severity": "warning"
                }
            ]
        })
        mock_run.return_value = mock_process
        
        # Call the method
        result = self.analyzer.analyze_go_code(self.test_repo_path)
        
        # Verify the result
        self.assertEqual(len(result['issues']), 1)
        self.assertEqual(result['issue_count'], 1)
        self.assertEqual(result['issues'][0]['type'], 'warning')
        self.assertEqual(result['issues'][0]['file'], 'test.go')
        self.assertEqual(result['issues'][0]['line'], 15)
        self.assertEqual(result['issues'][0]['message'], 'S1000: should use a simple channel send/receive instead of select with a single case')
    
    @patch('os.path.exists')
    @patch('subprocess.run')
    def test_analyze_rust_code(self, mock_run, mock_exists):
        """Test analyze_rust_code method"""
        # Set up the mocks
        mock_exists.return_value = True
        
        # Mock the subprocess.run result
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = json.dumps({
            "reason": "compiler-message",
            "message": {
                "rendered": "warning: unused variable: `x`\n --> src/main.rs:2:9\n  |\n2 |     let x = 5;\n  |         ^ help: if this is intentional, prefix it with an underscore: `_x`\n  |\n  = note: `#[warn(unused_variables)]` on by default\n\n",
                "children": [],
                "code": {
                    "code": "unused_variables",
                    "explanation": null
                },
                "level": "warning",
                "message": "unused variable: `x`",
                "spans": [
                    {
                        "byte_end": 26,
                        "byte_start": 25,
                        "column_end": 10,
                        "column_start": 9,
                        "expansion": null,
                        "file_name": "src/main.rs",
                        "is_primary": true,
                        "label": "help: if this is intentional, prefix it with an underscore: `_x`",
                        "line_end": 2,
                        "line_start": 2,
                        "suggested_replacement": "_x",
                        "suggestion_applicability": "MachineApplicable",
                        "text": [
                            {
                                "highlight_end": 10,
                                "highlight_start": 9,
                                "text": "    let x = 5;"
                            }
                        ]
                    }
                ]
            }
        })
        mock_run.return_value = mock_process
        
        # Call the method
        result = self.analyzer.analyze_rust_code(self.test_repo_path)
        
        # Verify the result
        self.assertEqual(len(result['issues']), 1)
        self.assertEqual(result['issue_count'], 1)
        self.assertEqual(result['issues'][0]['type'], 'warning')
        self.assertEqual(result['issues'][0]['file'], 'src/main.rs')
        self.assertEqual(result['issues'][0]['line'], 2)
        self.assertEqual(result['issues'][0]['message'], 'unused variable: `x`')
    
    def test_analyze_code(self):
        """Test analyze_code method"""
        # Mock the language-specific analysis methods
        self.analyzer.analyze_python_code = MagicMock(return_value={
            'issues': [{'type': 'convention', 'file': 'test.py', 'line': 10, 'message': 'Test issue'}],
            'issue_count': 1
        })
        self.analyzer.analyze_javascript_code = MagicMock(return_value={
            'issues': [{'type': 'error', 'file': 'test.js', 'line': 5, 'message': 'Test issue'}],
            'issue_count': 1
        })
        
        # Call the method
        result = self.analyzer.analyze_code(self.test_repo_path, ['Python', 'JavaScript'])
        
        # Verify the result
        self.assertEqual(len(result), 2)  # Two languages
        self.assertIn('Python', result)
        self.assertIn('JavaScript', result)
        self.assertEqual(result['Python']['issue_count'], 1)
        self.assertEqual(result['JavaScript']['issue_count'], 1)
        
        # Verify the method calls
        self.analyzer.analyze_python_code.assert_called_once_with(self.test_repo_path)
        self.analyzer.analyze_javascript_code.assert_called_once_with(self.test_repo_path)
    
    @patch('os.walk')
    def test_find_files(self, mock_walk):
        """Test _find_files method"""
        # Set up the mock
        mock_walk.return_value = [
            ('/test/repo', ['dir1'], ['file1.py', 'file2.js']),
            ('/test/repo/dir1', [], ['file3.py'])
        ]
        
        # Call the method
        python_files = self.analyzer._find_files(self.test_repo_path, '.py')
        
        # Verify the result
        self.assertEqual(len(python_files), 2)
        self.assertIn('/test/repo/file1.py', python_files)
        self.assertIn('/test/repo/dir1/file3.py', python_files)
    
    @patch('os.path.exists')
    def test_check_tool_availability(self, mock_exists):
        """Test _check_tool_availability method"""
        # Set up the mock
        mock_exists.side_effect = [True, False]  # First tool exists, second doesn't
        
        # Call the method
        result1 = self.analyzer._check_tool_availability('tool1')
        result2 = self.analyzer._check_tool_availability('tool2')
        
        # Verify the result
        self.assertTrue(result1)
        self.assertFalse(result2)
    
    @patch('subprocess.run')
    def test_run_command(self, mock_run):
        """Test _run_command method"""
        # Set up the mock
        mock_process = MagicMock()
        mock_process.returncode = 0
        mock_process.stdout = "Test output"
        mock_run.return_value = mock_process
        
        # Call the method
        returncode, output = self.analyzer._run_command(['test', 'command'])
        
        # Verify the result
        self.assertEqual(returncode, 0)
        self.assertEqual(output, "Test output")
        mock_run.assert_called_once()


if __name__ == "__main__":
    unittest.main()