import os
import subprocess
import unittest
from datetime import datetime

import numpy as np
from damaris.data.ADC_Result import ADC_Result


class TestADCResult(unittest.TestCase):
    @staticmethod
    def create_adc_result():
        x = np.array([0.0, 1.0, 2.0])
        y = [np.array([10, 20, 30]), np.array([15, 25, 35])]
        index = [(0, 1)]
        job_date = datetime(2023, 1, 1)
        desc = {"key": "value"}
        sampl_freq = 1000.0
        job_id = 11
        adc = ADC_Result(x, y, index, sampl_freq, desc, job_id, job_date)
        adc.set_nChannels(2)
        return adc

    def test_constructor_with_no_arguments(self):
        """Test initializer with no arguments."""
        adc = ADC_Result()
        self.assertFalse(adc.contains_data())
        self.assertEqual(adc.sampling_rate, 0)
        self.assertEqual(len(adc.index), 0)
        self.assertEqual(len(adc.x), 0)
        self.assertEqual(len(adc.y), 0)

    def test_constructor_with_arguments(self):
        """Test initializer with all arguments."""
        x = np.array([0.0, 1.0, 2.0])
        y = [np.array([10, 20, 30]), np.array([15, 25, 35])]
        index = [(0, 2)]
        sampl_freq = 1000.0
        desc = {"key1": "value1"}
        job_id = 1
        job_date = datetime(2023, 1, 1)

        adc = ADC_Result(x, y, index, sampl_freq, desc, job_id, job_date)

        self.assertTrue(adc.contains_data())
        self.assertEqual(adc.sampling_rate, sampl_freq)
        self.assertEqual(adc.x.all(), x.all())
        self.assertEqual(adc.y[0].all(), y[0].all())
        self.assertEqual(adc.description, desc)
        self.assertEqual(adc.job_id, job_id)
        self.assertEqual(adc.job_date, job_date)
        self.assertTrue(np.array_equal(adc.x, np.array(x, dtype="float32")))
        self.assertTrue(np.array_equal(adc.y[0], np.array(y[0], dtype="int16")))
        self.assertTrue(np.array_equal(adc.y[1], np.array(y[1], dtype="int16")))

    def test_create_data_space(self):
        """Test creating a new data space."""
        adc = ADC_Result()
        adc.create_data_space(channels=2, samples=3)

        self.assertTrue(adc.contains_data())
        self.assertEqual(len(adc.y), 2)
        self.assertEqual(len(adc.x), 3)
        self.assertTrue(np.array_equal(adc.x, np.zeros(3, dtype="float32")))
        self.assertTrue(np.array_equal(adc.y[0], np.zeros(3, dtype="int16")))
        self.assertEqual(adc.index, [(0, 2)])

    def test_add_sample_space(self):
        """Test adding sample space."""
        adc = ADC_Result()
        adc.create_data_space(channels=1, samples=3)
        adc.add_sample_space(2)

        self.assertEqual(len(adc.x), 5)
        self.assertEqual(len(adc.y[0]), 5)
        self.assertEqual(adc.index[-1], (3, 4))

    def test_get_result_by_index(self):
        """Test retrieving data by index."""
        adc = self.create_adc_result()
        adc.index = [(0, 1), (1, 2)]
        adc.job_id = 42
        sub_result = adc.get_result_by_index(1)

        self.assertEqual(sub_result.sampling_rate, 1000.0)
        self.assertTrue(np.array_equal(sub_result.x, np.array([1.0, 2.0])))
        self.assertTrue(np.array_equal(sub_result.y[0], np.array([20, 30])))
        self.assertTrue(np.array_equal(sub_result.y[1], np.array([25, 35])))
        self.assertEqual(sub_result.index, [(0, 1)])
        self.assertEqual(sub_result.description, {"key": "value"})
        self.assertEqual(sub_result.job_id, 42)

    def test_set_sampling_rate(self):
        """Test updating the sampling rate."""
        adc = ADC_Result()
        adc.set_sampling_rate(5000.0)
        self.assertEqual(adc.get_sampling_rate(), 5000.0)

    def test_set_nChannels(self):
        """Test setting the number of channels."""
        adc = ADC_Result()
        adc.set_nChannels(5)
        self.assertEqual(adc.get_nChannels(), 5)

    def test_write_to_csv(self):
        """Test the functionality of writing to CSV."""
        from io import StringIO
        x = np.array([0.0, 1.0])
        y = [np.array([10, 20]), np.array([15, 25])]
        index = [(0, 1)]
        job_date = datetime(2023, 1, 1)
        desc = {"key": "value"}
        sampl_freq = 1000.0
        job_id=9
        adc = ADC_Result(x, y, index, sampl_freq, desc, job_id, job_date)
        output = StringIO()

        adc.write_to_csv(output, delimiter=",")
        content = output.getvalue()
        expected = (
            "# adc_result\n"
            "# t y0 y1 ...\n"
            "0.000000e+00,1.000000e+01,1.500000e+01\n"
            "1.000000e+00,2.000000e+01,2.500000e+01\n"
        )
        self.assertEqual(content, expected)


    def test_write_to_hdf(self):
        """Test the functionality of writing to HDF."""
        import tables
        # do not change the values here, or you need to recreate the h5dump with new values
        x = np.array([0.0, 1.0])
        y = [np.array([10, 20]), np.array([15, 25])]
        index = [(0, 1)]
        job_date = datetime(2023, 1, 1)
        desc = {"key": "value"}
        sampl_freq = 1000.0
        job_id = 10
        adc = ADC_Result(x, y, index, sampl_freq, desc, job_id, job_date)
        # write out data
        hdffile = tables.open_file("test.hdf5", mode="w")
        adc.write_to_hdf(hdffile, where="/", name="name", title="title", complib="zlib", complevel=3)
        hdffile.close()
        # read back data with h5dump utility (apt-get -y install hdf5-tools)
        h5dump = subprocess.run(["h5dump", "-d", "/name/adc_data",  "test.hdf5"], capture_output=True)
        content = h5dump.stdout.decode("utf-8")
        test_dir = os.path.dirname(__file__)
        with open(os.path.join(test_dir, "h5dump1_adc_data.ascii"), "r") as f:
            expected = f.read()
        self.assertEqual(content, expected)
        os.unlink("test.hdf5")

    def test_operator_len(self):
        """Test the functionality of __len__"""
        adc = self.create_adc_result()
        self.assertEqual(len(adc), 3)

    def test_operator_add_scalar(self):
        """
        Test the functionality of __add__ and __radd__
        """
        adc = self.create_adc_result()
        y = adc.y
        # test integer addition
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i]+10, y[i] + 10, strict=True)
        adc += 10
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] + 10, strict=True)

        # test float addition
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i] + 10., y[i] + 10., strict=True)
        adc += 10.
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] + 10 + 10., strict=True)

    def test_operator_sub_scalar(self):
        """
        Test the functionality of __sub__ and __rsub__
        """
        adc = self.create_adc_result()
        y = adc.y
        # test integer subtraction
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i] - 10, y[i] - 10, strict=True)
        adc -= 10
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] - 10, strict=True)

        # test float subtraction
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i] - 10., y[i] - 10., strict=True)
        adc -= 10.
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] - 10 - 10., strict=True)

    def test_operator_mul_scalar(self):
        """
        Test the functionality of __mul and __rmul__
        """

        adc = self.create_adc_result()
        y = adc.y
        # test integer multiplication
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i] * 10, y[i] * 10, strict=True)
        adc *= 10
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] * 10, strict=True)

        # test float multiplication
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i] * 10.0, y[i] * 10.0, strict=True)
        adc *= 10.0
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] * 10 * 10.0, strict=True)

    def test_operator_truediv_scalar(self):
        """
        Test the functionality of __div__ and __rdiv__
        """
        adc = self.create_adc_result()
        y = adc.y
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i]/10, y[i] / 10, strict=True)
        adc /= 10
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] / 10, strict=True)

    def test_operator_floordiv_scalar(self):
        """
        Test the functionality of __div__ and __rdiv__
        """
        adc = self.create_adc_result()
        y = adc.y
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i]//10, y[i] // 10, strict=True)
        adc //= 10
        for i in range(adc.get_nChannels()):
            np.testing.assert_array_equal(adc.y[i], y[i] // 10, strict=True)
        self.assertRaises(ValueError, adc.__floordiv__, 1.0)
        self.assertRaises(ValueError, adc.__rfloordiv__, 1.0)

    def test_operator_overload_adc_result_exception(self):
        """
        Test the ValueError raised when ADC_Result is used as an operand in an arithmetic operation
        """
        adc1 = self.create_adc_result()
        adc2 = self.create_adc_result()
        self.assertRaises(ValueError, adc1.__add__, adc2)
        self.assertRaises(ValueError, adc1.__radd__, adc2)
        self.assertRaises(ValueError, adc1.__sub__, adc2)
        self.assertRaises(ValueError, adc1.__rsub__, adc2)
        self.assertRaises(ValueError, adc1.__mul__, adc2)
        self.assertRaises(ValueError, adc1.__rmul__, adc2)
        self.assertRaises(ValueError, adc1.__truediv__, adc2)
        self.assertRaises(ValueError, adc1.__rtruediv__, adc2)
        self.assertRaises(ValueError, adc1.__floordiv__, adc2)
        self.assertRaises(ValueError, adc1.__rfloordiv__, adc2)
        self.assertRaises(ValueError, adc1.__pow__, adc2)

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