#!/usr/bin/env python
#
# Glances - An eye on your system
#
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#

"""Tests for the JSON serializer module.

These tests are designed to be runnable standalone without requiring
the full Glances initialization, for faster CI feedback.
"""

import json
import os
import sys
import types
import unittest
from datetime import datetime
from unittest.mock import Mock


def setup_mock_modules():
    """Set up mock modules to allow importing the serializer without full Glances."""
    # Create mock glances package
    if 'glances' not in sys.modules:
        glances_pkg = types.ModuleType('glances')
        glances_pkg.__path__ = [os.path.join(os.path.dirname(__file__), '..', 'glances')]
        sys.modules['glances'] = glances_pkg

    # Create mock glances.outputs package
    if 'glances.outputs' not in sys.modules:
        glances_outputs = types.ModuleType('glances.outputs')
        sys.modules['glances.outputs'] = glances_outputs

    # Create mock globals
    if 'glances.globals' not in sys.modules:
        glances_globals = types.ModuleType('glances.globals')
        glances_globals.json_dumps = lambda data: json.dumps(data, default=str).encode('utf-8')
        glances_globals.json_loads = lambda data: json.loads(data.decode('utf-8') if isinstance(data, bytes) else data)
        sys.modules['glances.globals'] = glances_globals

    # Create mock logger
    if 'glances.logger' not in sys.modules:

        class MockLogger:
            def debug(self, msg):
                pass

            def error(self, msg):
                pass

            def info(self, msg):
                pass

        glances_logger = types.ModuleType('glances.logger')
        glances_logger.logger = MockLogger()
        sys.modules['glances.logger'] = glances_logger


# Setup mocks BEFORE any other imports
setup_mock_modules()

# Now we can import our serializer
# Direct exec to avoid import conflicts
serializer_path = os.path.join(os.path.dirname(__file__), '..', 'glances', 'outputs', 'glances_json_serializer.py')
with open(serializer_path) as f:
    code = f.read()
    exec(compile(code, serializer_path, 'exec'), sys.modules['glances.outputs'].__dict__)

PluginSerializationError = sys.modules['glances.outputs'].PluginSerializationError
GlancesJSONSerializer = sys.modules['glances.outputs'].GlancesJSONSerializer


class TestPluginSerializationError(unittest.TestCase):
    """Test the PluginSerializationError class."""

    def test_error_to_dict(self):
        error = PluginSerializationError("cpu", "Test error")
        result = error.to_dict()

        self.assertEqual(result["error"], True)
        self.assertEqual(result["plugin"], "cpu")
        self.assertEqual(result["message"], "Test error")


class TestGlancesJSONSerializer(unittest.TestCase):
    """Test the GlancesJSONSerializer class."""

    def setUp(self):
        self.serializer = GlancesJSONSerializer()

    def test_normalize_none(self):
        self.assertIsNone(self.serializer.normalize_value(None))

    def test_normalize_bytes(self):
        result = self.serializer.normalize_value(b'hello')
        self.assertEqual(result, 'hello')

    def test_normalize_datetime(self):
        dt = datetime(2024, 1, 15, 10, 30, 0)
        result = self.serializer.normalize_value(dt)
        self.assertEqual(result, '2024-01-15T10:30:00')

    def test_normalize_primitives(self):
        self.assertEqual(self.serializer.normalize_value(42), 42)
        self.assertEqual(self.serializer.normalize_value(3.14), 3.14)
        self.assertEqual(self.serializer.normalize_value('test'), 'test')
        self.assertEqual(self.serializer.normalize_value(True), True)

    def test_normalize_dict(self):
        data = {'a': 1, 'b': b'bytes', 'c': None}
        result = self.serializer.normalize_value(data)

        self.assertEqual(result['a'], 1)
        self.assertEqual(result['b'], 'bytes')
        self.assertIsNone(result['c'])

    def test_normalize_list(self):
        data = [1, b'bytes', 'string', None]
        result = self.serializer.normalize_value(data)

        self.assertEqual(result, [1, 'bytes', 'string', None])

    def test_normalize_nested(self):
        data = {'outer': {'inner': [1, 2, {'deep': b'value'}]}}
        result = self.serializer.normalize_value(data)

        self.assertEqual(result['outer']['inner'][2]['deep'], 'value')

    def test_serialize_plugin_data_none(self):
        result = self.serializer.serialize_plugin_data('test', None)
        self.assertIsNone(result)

    def test_serialize_plugin_data_bytes_json(self):
        data = b'{"cpu": 50, "memory": 75}'
        result = self.serializer.serialize_plugin_data('test', data)

        self.assertEqual(result['cpu'], 50)
        self.assertEqual(result['memory'], 75)

    def test_serialize_plugin_data_bytes_invalid_json(self):
        data = b'not valid json'
        result = self.serializer.serialize_plugin_data('test', data)
        self.assertEqual(result, 'not valid json')

    def test_serialize_plugin_data_dict(self):
        data = {'cpu': 50, 'memory': 75}
        result = self.serializer.serialize_plugin_data('test', data)

        self.assertEqual(result['cpu'], 50)
        self.assertEqual(result['memory'], 75)

    def test_to_json_string_dict(self):
        data = {'key': 'value', 'number': 42}
        result = self.serializer.to_json_string(data)

        parsed = json.loads(result)
        self.assertEqual(parsed['key'], 'value')
        self.assertEqual(parsed['number'], 42)

    def test_to_json_string_list(self):
        data = [1, 2, 3, 'four']
        result = self.serializer.to_json_string(data)

        parsed = json.loads(result)
        self.assertEqual(parsed, [1, 2, 3, 'four'])

    def test_serialize_plugins_empty_list(self):
        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = []

        result = self.serializer.serialize_plugins(mock_stats, [])
        self.assertEqual(result, {})

    def test_serialize_plugins_with_data(self):
        mock_plugin = Mock()
        mock_plugin.is_enabled.return_value = True
        mock_plugin.get_json.return_value = b'{"value": 42}'

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['test_plugin']
        mock_stats.get_plugin.return_value = mock_plugin

        result = self.serializer.serialize_plugins(mock_stats, ['test_plugin'])

        self.assertIn('test_plugin', result)
        self.assertEqual(result['test_plugin']['value'], 42)

    def test_serialize_plugins_plugin_not_enabled(self):
        mock_plugin = Mock()
        mock_plugin.is_enabled.return_value = False

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['disabled_plugin']
        mock_stats.get_plugin.return_value = mock_plugin

        result = self.serializer.serialize_plugins(mock_stats, ['disabled_plugin'])
        self.assertNotIn('disabled_plugin', result)

    def test_serialize_plugins_plugin_not_found(self):
        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = []
        mock_stats.get_plugin.return_value = None

        result = self.serializer.serialize_plugins(mock_stats, ['nonexistent'])
        self.assertNotIn('nonexistent', result)

    def test_serialize_plugins_with_metadata(self):
        serializer = GlancesJSONSerializer(include_metadata=True)

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = []

        result = serializer.serialize_plugins(mock_stats, [])

        self.assertIn('_metadata', result)
        self.assertIn('timestamp', result['_metadata'])
        self.assertIn('plugin_count', result['_metadata'])

    def test_serialize_to_string_produces_valid_json(self):
        mock_plugin = Mock()
        mock_plugin.is_enabled.return_value = True
        mock_plugin.get_json.return_value = b'{"cpu": 25.5}'

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['cpu']
        mock_stats.get_plugin.return_value = mock_plugin

        result = self.serializer.serialize_to_string(mock_stats, ['cpu'])

        parsed = json.loads(result)
        self.assertIn('cpu', parsed)

    def test_serialize_handles_unicode(self):
        data = {'name': 'café ☕', 'value': 42}
        result = self.serializer.serialize_plugin_data('test', data)

        self.assertEqual(result['name'], 'café ☕')


class TestSerializerEdgeCases(unittest.TestCase):
    """Test edge cases and error handling."""

    def test_serializer_with_errors_disabled(self):
        serializer = GlancesJSONSerializer(include_errors=False)

        mock_plugin = Mock()
        mock_plugin.is_enabled.return_value = True
        mock_plugin.get_json.side_effect = Exception("Plugin error")

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['failing_plugin']
        mock_stats.get_plugin.return_value = mock_plugin

        result = serializer.serialize_plugins(mock_stats, ['failing_plugin'])
        self.assertNotIn('_errors', result)
        self.assertNotIn('failing_plugin', result)

    def test_serializer_with_errors_enabled(self):
        serializer = GlancesJSONSerializer(include_errors=True)

        mock_plugin = Mock()
        mock_plugin.is_enabled.return_value = True
        mock_plugin.get_json.side_effect = Exception("Plugin error")

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['failing_plugin']
        mock_stats.get_plugin.return_value = mock_plugin

        result = serializer.serialize_plugins(mock_stats, ['failing_plugin'])
        self.assertIn('_errors', result)

    def test_multiple_plugins_one_fails(self):
        """Verify that one plugin failing doesn't break the entire output."""
        serializer = GlancesJSONSerializer(include_errors=True)

        good_plugin = Mock()
        good_plugin.is_enabled.return_value = True
        good_plugin.get_json.return_value = b'{"status": "ok"}'

        bad_plugin = Mock()
        bad_plugin.is_enabled.return_value = True
        bad_plugin.get_json.side_effect = Exception("Failed")

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['good', 'bad']

        def get_plugin(name):
            return good_plugin if name == 'good' else bad_plugin

        mock_stats.get_plugin.side_effect = get_plugin

        result = serializer.serialize_plugins(mock_stats, ['good', 'bad'])

        self.assertIn('good', result)
        self.assertEqual(result['good']['status'], 'ok')
        self.assertIn('_errors', result)

    def test_empty_bytes_input(self):
        serializer = GlancesJSONSerializer()
        result = serializer.serialize_plugin_data('test', b'')
        self.assertEqual(result, '')

    def test_all_plugins_fail_still_produces_valid_json(self):
        """Verify that even total failure produces parseable JSON."""
        serializer = GlancesJSONSerializer(include_errors=True)

        failing_plugin = Mock()
        failing_plugin.is_enabled.return_value = True
        failing_plugin.get_json.side_effect = Exception("Total failure")

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['fail1', 'fail2']
        mock_stats.get_plugin.return_value = failing_plugin

        result = serializer.serialize_to_string(mock_stats, ['fail1', 'fail2'])
        parsed = json.loads(result)  # Should not raise
        self.assertIn('_errors', parsed)

    def test_empty_plugins_produces_valid_json(self):
        """Verify that empty plugin list produces valid JSON."""
        serializer = GlancesJSONSerializer()

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = []

        result = serializer.serialize_to_string(mock_stats, [])
        parsed = json.loads(result)
        self.assertEqual(parsed, {})

    def test_output_structure_consistency(self):
        """Verify the output structure is consistent."""
        serializer = GlancesJSONSerializer(include_errors=True, include_metadata=True)

        mock_plugin = Mock()
        mock_plugin.is_enabled.return_value = True
        mock_plugin.get_json.return_value = b'{"value": 100}'

        mock_stats = Mock()
        mock_stats.getPluginsList.return_value = ['cpu']
        mock_stats.get_plugin.return_value = mock_plugin

        result = serializer.serialize_plugins(mock_stats, ['cpu'])

        # Verify structure
        self.assertIsInstance(result, dict)
        self.assertIn('cpu', result)
        self.assertIn('_metadata', result)
        self.assertIsInstance(result['_metadata'], dict)
        self.assertIn('timestamp', result['_metadata'])
        self.assertIn('plugin_count', result['_metadata'])


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