"""
Unit tests for dicompare.serialization module.
"""

import unittest
import numpy as np
import pandas as pd
import json

from dicompare.io import make_json_serializable


class TestSerialization(unittest.TestCase):
    """Test cases for JSON serialization utilities."""
    
    def test_basic_types_passthrough(self):
        """Test that basic JSON-serializable types pass through unchanged."""
        # Basic types
        self.assertEqual(make_json_serializable(42), 42)
        self.assertEqual(make_json_serializable(3.14), 3.14)
        self.assertEqual(make_json_serializable("hello"), "hello")
        self.assertEqual(make_json_serializable(True), True)
        self.assertEqual(make_json_serializable(False), False)
        self.assertEqual(make_json_serializable(None), None)
    
    def test_numpy_arrays(self):
        """Test conversion of numpy arrays to lists."""
        # 1D array
        arr_1d = np.array([1, 2, 3, 4])
        result = make_json_serializable(arr_1d)
        self.assertEqual(result, [1, 2, 3, 4])
        
        # 2D array
        arr_2d = np.array([[1, 2], [3, 4]])
        result = make_json_serializable(arr_2d)
        self.assertEqual(result, [[1, 2], [3, 4]])
        
        # Empty array
        arr_empty = np.array([])
        result = make_json_serializable(arr_empty)
        self.assertEqual(result, [])
        
        # Different dtypes
        arr_float = np.array([1.1, 2.2, 3.3])
        result = make_json_serializable(arr_float)
        self.assertEqual(result, [1.1, 2.2, 3.3])
    
    def test_numpy_scalars(self):
        """Test conversion of numpy scalar types."""
        # Integer types
        self.assertEqual(make_json_serializable(np.int32(42)), 42)
        self.assertEqual(make_json_serializable(np.int64(42)), 42)
        
        # Float types
        self.assertEqual(make_json_serializable(np.float32(3.14)), 3.140000104904175)  # float32 precision
        self.assertEqual(make_json_serializable(np.float64(3.14)), 3.14)
        
        # Boolean type
        self.assertEqual(make_json_serializable(np.bool_(True)), True)
        self.assertEqual(make_json_serializable(np.bool_(False)), False)
    
    def test_numpy_special_values(self):
        """Test handling of NaN and infinity values."""
        # NaN values should become None
        self.assertIsNone(make_json_serializable(np.nan))
        self.assertIsNone(make_json_serializable(np.float64(np.nan)))
        
        # Infinity values should become None
        self.assertIsNone(make_json_serializable(np.inf))
        self.assertIsNone(make_json_serializable(-np.inf))
        self.assertIsNone(make_json_serializable(np.float64(np.inf)))
    
    def test_pandas_series(self):
        """Test conversion of pandas Series to lists."""
        series = pd.Series([1, 2, 3, 4])
        result = make_json_serializable(series)
        self.assertEqual(result, [1, 2, 3, 4])
        
        # Series with NaN
        series_nan = pd.Series([1, np.nan, 3])
        result = make_json_serializable(series_nan)
        # pandas Series.tolist() preserves np.nan as nan, so we get [1.0, nan, 3.0]
        # This is expected behavior - our serialization function should handle this
        expected = [1.0, float('nan'), 3.0]
        self.assertEqual(len(result), 3)
        self.assertEqual(result[0], 1.0)
        self.assertTrue(np.isnan(result[1]))  # Check nan separately
        self.assertEqual(result[2], 3.0)
    
    def test_pandas_dataframe(self):
        """Test conversion of pandas DataFrame to list of records."""
        df = pd.DataFrame({
            'A': [1, 2, 3],
            'B': ['x', 'y', 'z'],
            'C': [1.1, 2.2, 3.3]
        })
        result = make_json_serializable(df)
        expected = [
            {'A': 1, 'B': 'x', 'C': 1.1},
            {'A': 2, 'B': 'y', 'C': 2.2},
            {'A': 3, 'B': 'z', 'C': 3.3}
        ]
        self.assertEqual(result, expected)
    
    def test_pandas_na_values(self):
        """Test handling of pandas NA values."""
        # Test pd.NA
        self.assertIsNone(make_json_serializable(pd.NA))
        
        # Test pd.NaT (Not a Time)
        self.assertIsNone(make_json_serializable(pd.NaT))
    
    def test_nested_structures(self):
        """Test recursive processing of nested data structures."""
        # Nested dictionary with numpy arrays
        data = {
            'array': np.array([1, 2, 3]),
            'scalar': np.int64(42),
            'nested': {
                'series': pd.Series([4, 5, 6]),
                'value': np.float32(3.14)
            }
        }
        result = make_json_serializable(data)
        expected = {
            'array': [1, 2, 3],
            'scalar': 42,
            'nested': {
                'series': [4, 5, 6],
                'value': 3.140000104904175
            }
        }
        self.assertEqual(result, expected)
    
    def test_list_and_tuple_conversion(self):
        """Test conversion of lists and tuples containing numpy/pandas objects."""
        # List with mixed types
        data_list = [np.array([1, 2]), np.int64(42), pd.Series([3, 4])]
        result = make_json_serializable(data_list)
        expected = [[1, 2], 42, [3, 4]]
        self.assertEqual(result, expected)
        
        # Tuple (should become list)
        data_tuple = (np.array([1, 2]), np.int64(42))
        result = make_json_serializable(data_tuple)
        expected = [[1, 2], 42]
        self.assertEqual(result, expected)
    
    def test_float_special_values(self):
        """Test handling of Python float special values."""
        # Python float NaN and infinity
        self.assertIsNone(make_json_serializable(float('nan')))
        self.assertIsNone(make_json_serializable(float('inf')))
        self.assertIsNone(make_json_serializable(float('-inf')))
        
        # Normal float should pass through
        self.assertEqual(make_json_serializable(3.14), 3.14)
    
    def test_complex_real_world_example(self):
        """Test with a complex real-world data structure."""
        data = {
            'metadata': {
                'name': 'test_acquisition',
                'total_files': np.int64(150),
                'matrix_size': np.array([256, 256]),
                'has_issues': np.bool_(False)
            },
            'statistics': {
                'mean_values': pd.Series([1.1, 2.2, 3.3]),
                'ranges': np.array([[0, 10], [5, 15]]),
                'missing_count': np.int32(5)
            },
            'results': pd.DataFrame({
                'field': ['EchoTime', 'RepetitionTime'],
                'value': [0.01, 2000.0],
                'compliant': [True, False]
            }),
            'special_values': [np.nan, np.inf, pd.NA]
        }
        
        result = make_json_serializable(data)
        
        # Verify structure is preserved
        self.assertIn('metadata', result)
        self.assertIn('statistics', result)
        self.assertIn('results', result)
        self.assertIn('special_values', result)
        
        # Verify specific conversions
        self.assertEqual(result['metadata']['total_files'], 150)
        self.assertEqual(result['metadata']['matrix_size'], [256, 256])
        self.assertEqual(result['metadata']['has_issues'], False)
        
        self.assertEqual(result['statistics']['mean_values'], [1.1, 2.2, 3.3])
        self.assertEqual(result['statistics']['ranges'], [[0, 10], [5, 15]])
        self.assertEqual(result['statistics']['missing_count'], 5)
        
        # DataFrame should become list of records
        self.assertEqual(len(result['results']), 2)
        self.assertEqual(result['results'][0]['field'], 'EchoTime')
        
        # Special values should all become None
        self.assertEqual(result['special_values'], [None, None, None])
    
    def test_json_serializable_output(self):
        """Test that the output can actually be JSON serialized."""
        data = {
            'array': np.array([1, 2, 3]),
            'scalar': np.int64(42),
            'series': pd.Series([4, 5, 6]),
            'dataframe': pd.DataFrame({'A': [1, 2], 'B': [3, 4]}),
            'nan_value': np.nan
        }
        
        result = make_json_serializable(data)
        
        # This should not raise an exception
        json_string = json.dumps(result)
        
        # Verify we can load it back
        loaded = json.loads(json_string)
        self.assertEqual(loaded['array'], [1, 2, 3])
        self.assertEqual(loaded['scalar'], 42)
        self.assertEqual(loaded['series'], [4, 5, 6])
        self.assertIsNone(loaded['nan_value'])
    
    def test_empty_structures(self):
        """Test handling of empty data structures."""
        # Empty dict
        self.assertEqual(make_json_serializable({}), {})
        
        # Empty list
        self.assertEqual(make_json_serializable([]), [])
        
        # Empty numpy array
        self.assertEqual(make_json_serializable(np.array([])), [])
        
        # Empty pandas Series
        self.assertEqual(make_json_serializable(pd.Series([])), [])
        
        # Empty pandas DataFrame
        result = make_json_serializable(pd.DataFrame())
        self.assertEqual(result, [])
    
    def test_object_with_item_method(self):
        """Test handling of objects with item() method."""
        # Create an object with item() method (like numpy scalars)
        class ObjectWithItemMethod:
            def item(self):
                return 42

        obj = ObjectWithItemMethod()
        result = make_json_serializable(obj)
        self.assertEqual(result, 42)

    def test_object_with_item_method_exception(self):
        """Test handling when item() method raises an exception."""
        class ObjectWithFailingItemMethod:
            def item(self):
                raise ValueError("Cannot convert")

        obj = ObjectWithFailingItemMethod()
        # Should raise the exception (fail fast philosophy)
        with self.assertRaises(ValueError):
            make_json_serializable(obj)
    
    def test_deeply_nested_structure(self):
        """Test with deeply nested structures."""
        data = {
            'level1': {
                'level2': {
                    'level3': {
                        'array': np.array([1, 2, 3]),
                        'list': [np.int64(4), np.float32(5.5)]
                    }
                }
            }
        }
        
        result = make_json_serializable(data)
        expected = {
            'level1': {
                'level2': {
                    'level3': {
                        'array': [1, 2, 3],
                        'list': [4, 5.5]
                    }
                }
            }
        }
        self.assertEqual(result, expected)


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