import contextlib
import logging
import os
import unittest
from io import StringIO

import h5py
import numpy as np
import pandas as pd

from evefile.boundaries import evefile
import evefile.entities.data
import evefile.entities.file


class DummyHDF5File:
    def __init__(self, filename=""):
        self.filename = filename

    def create(self, set_preferred=False, add_snapshot=False):
        with h5py.File(self.filename, "w") as file:
            file.attrs["EVEH5Version"] = np.bytes_(["7"])
            file.attrs["Version"] = np.bytes_(["2.0"])
            file.attrs["XMLversion"] = np.bytes_(["9.2"])
            file.attrs["Comment"] = np.bytes_([""])
            file.attrs["Location"] = np.bytes_(["Unittest"])
            file.attrs["StartTimeISO"] = np.bytes_(["2024-06-03T12:01:32"])
            file.attrs["EndTimeISO"] = np.bytes_(["2024-06-03T12:01:37"])
            file.attrs["Simulation"] = np.bytes_(["no"])
            monitors = file.create_group("device")
            simmon = monitors.create_dataset(
                "SimMonitor:01.STAT",
                data=np.ones(
                    [5],
                    dtype=np.dtype(
                        [
                            ("mSecsSinceStart", "<i4"),
                            ("SimMonitor:01.STAT", "<f8"),
                        ]
                    ),
                ),
            )
            simmon["mSecsSinceStart"] = np.asarray([-1, -1, 2000, 6200, 9100])
            simmon["SimMonitor:01.STAT"] = np.random.random(5)
            simmon.attrs["Name"] = np.bytes_(["Status"])
            simmon.attrs["Access"] = np.bytes_(["ca:foobar"])
            c1 = file.create_group("c1")
            main = c1.create_group("main")
            meta = c1.create_group("meta")
            simmot = main.create_dataset(
                "SimMot:01",
                data=np.ones(
                    [5],
                    dtype=np.dtype(
                        [("PosCounter", "<i4"), ("SimMot:01", "<f8")]
                    ),
                ),
            )
            simmot["PosCounter"] = np.linspace(2, 6, 5)
            simmot["SimMot:01"] = np.random.random(5)
            simmot.attrs["Name"] = np.bytes_(["foo"])
            simmot.attrs["Access"] = np.bytes_(["ca:foobar"])
            simmot.attrs["DeviceType"] = np.bytes_(["Axis"])
            simchan = main.create_dataset(
                "SimChan:01",
                data=np.ones(
                    [5],
                    dtype=np.dtype(
                        [("PosCounter", "<i4"), ("SimChan:01", "<f8")]
                    ),
                ),
            )
            simchan["PosCounter"] = np.linspace(4, 8, 5)
            simchan["SimChan:01"] = np.random.random(5)
            simchan.attrs["Name"] = np.bytes_(["bar"])
            simchan.attrs["Access"] = np.bytes_(["ca:barbaz"])
            simchan.attrs["DeviceType"] = np.bytes_(["Channel"])
            simchan.attrs["Detectortype"] = np.bytes_(["Standard"])
            data = np.ndarray(
                [9],
                dtype=np.dtype(
                    [("PosCounter", "<i4"), ("PosCountTimer", "<i4")]
                ),
            )
            poscounttimer = meta.create_dataset("PosCountTimer", data=data)
            poscounttimer.attrs["Unit"] = np.bytes_(["msecs"])
            if set_preferred:
                c1.attrs["preferredAxis"] = np.bytes_(["SimMot:01"])
                c1.attrs["preferredChannel"] = np.bytes_(["SimChan:01"])
                c1.attrs["preferredNormalizationChannel"] = np.bytes_(
                    ["SimChan:01"]
                )
            if add_snapshot:
                snapshot = c1.create_group("snapshot")
                simmot = snapshot.create_dataset(
                    "SimMot:01",
                    data=np.ndarray(
                        [2],
                        dtype=np.dtype(
                            [("PosCounter", "<i4"), ("SimMot:01", "<f8")]
                        ),
                    ),
                )
                simmot["PosCounter"] = np.asarray([1, 9])
                simmot["SimMot:01"] = np.random.random(2)
                simmot.attrs["Name"] = np.bytes_(["foo"])
                simmot.attrs["Unit"] = np.bytes_(["eV"])
                simmot.attrs["Access"] = np.bytes_(["ca:foobar"])
                simmot.attrs["DeviceType"] = np.bytes_(["Axis"])
                simchan = snapshot.create_dataset(
                    "SimChan:01",
                    data=np.ndarray(
                        [2],
                        dtype=np.dtype(
                            [("PosCounter", "<i4"), ("SimChan:01", "<f8")]
                        ),
                    ),
                )
                simchan["PosCounter"] = np.asarray([1, 9])
                simchan["SimChan:01"] = np.random.random(2)
                simchan.attrs["Name"] = np.bytes_(["bar"])
                simchan.attrs["Unit"] = np.bytes_(["A"])
                simchan.attrs["Access"] = np.bytes_(["ca:barbaz"])
                simchan.attrs["DeviceType"] = np.bytes_(["Channel"])
                simchan.attrs["Detectortype"] = np.bytes_(["Standard"])
                simchan3 = snapshot.create_dataset(
                    "SimChan:03",
                    data=np.ndarray(
                        [2],
                        dtype=np.dtype(
                            [("PosCounter", "<i4"), ("SimChan:03", "<f8")]
                        ),
                    ),
                )
                simchan3["PosCounter"] = np.asarray([1, 9])
                simchan3["SimChan:03"] = np.random.random(2)
                simchan3.attrs["Name"] = np.bytes_(["bazfoo"])
                simchan3.attrs["Unit"] = np.bytes_(["A"])
                simchan3.attrs["Access"] = np.bytes_(["ca:bazfoo"])
                simchan3.attrs["DeviceType"] = np.bytes_(["Channel"])
                simchan3.attrs["Detectortype"] = np.bytes_(["Standard"])


class TestEveFile(unittest.TestCase):
    def setUp(self):
        self.filename = "file.h5"
        self.evefile = evefile.EveFile(filename=self.filename, load=False)
        self.logger = logging.getLogger(name="evedata")

    def tearDown(self):
        if os.path.exists(self.filename):
            os.remove(self.filename)

    def test_instantiate_class(self):
        evefile.EveFile(load=False)

    def test_has_attributes(self):
        attributes = [
            "metadata",
            "log_messages",
            "data",
            "snapshots",
            "monitors",
            "position_timestamps",
            "filename",
        ]
        for attribute in attributes:
            with self.subTest(attribute=attribute):
                self.assertTrue(hasattr(self.evefile, attribute))

    def test_setting_filename_sets_metadata_filename(self):
        filename = "foobar.h5"
        self.evefile.filename = filename
        self.assertEqual(self.evefile.metadata.filename, filename)

    def test_init_with_filename_sets_metadata_filename(self):
        filename = "foobar.h5"
        file = evefile.EveFile(filename=filename, load=False)
        self.assertEqual(filename, file.metadata.filename)

    def test_init_with_load_and_missing_filename_raises(self):
        with self.assertRaisesRegex(ValueError, "No filename given"):
            evefile.EveFile(filename="", load=True)

    def test_init_with_load_and_nonexisting_file_raises(self):
        filename = "nonexisting.h5"
        with self.assertRaisesRegex(
            FileNotFoundError, f"File {filename} does " f"not " f"exist."
        ):
            evefile.EveFile(filename=filename, load=True)

    def test_load_sets_file_metadata(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        root_mappings = {
            "eveh5_version": "7",
            "measurement_station": "Unittest",
        }
        for key, value in root_mappings.items():
            with self.subTest(key=key, val=value):
                self.assertEqual(getattr(self.evefile.metadata, key), value)

    def test_get_data_returns_data_by_name(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertEqual(
            self.evefile.data["SimMot:01"],
            self.evefile.get_data("foo"),
        )

    def test_get_data_list_returns_data_by_name_as_array(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertEqual(
            self.evefile.data["SimMot:01"],
            self.evefile.get_data(["foo", "bar"])[0],
        )

    def test_data_have_correct_shape(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertEqual(5, len(self.evefile.data["SimChan:01"].data))
        self.assertEqual(5, len(self.evefile.data["SimMot:01"].data))

    def test_get_data_names(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertEqual(
            [item.metadata.name for item in self.evefile.data.values()],
            self.evefile.get_data_names(),
        )

    def test_get_preferred_data_returns_list(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(set_preferred=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertIsInstance(self.evefile.get_preferred_data(), list)

    def test_get_preferred_data_contains_datasets(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(set_preferred=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertIsInstance(
            self.evefile.get_preferred_data()[0],
            evefile.entities.data.AxisData,
        )
        self.assertIsInstance(
            self.evefile.get_preferred_data()[1],
            evefile.entities.data.ChannelData,
        )
        self.assertIsInstance(
            self.evefile.get_preferred_data()[2],
            evefile.entities.data.ChannelData,
        )

    def test_get_preferred_data_without_preferences_returns_none(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(set_preferred=False)
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertListEqual(
            self.evefile.get_preferred_data(), [None, None, None]
        )

    def test_get_preferred_data_with_missing_channel_logs_warning(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(set_preferred=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        self.evefile.metadata.preferred_channel = "foo"
        self.logger.setLevel(logging.WARNING)
        self.logger.addHandler(logging.NullHandler())
        with self.assertLogs(level=logging.WARNING) as captured:
            preferred_data = self.evefile.get_preferred_data()
            self.assertEqual(len(captured.records), 1)
            self.assertEqual(
                captured.records[0].getMessage(),
                "Dataset foo not found, possibly an attribute or option?",
            )
        self.assertIsNone(preferred_data[1])

    def test_get_joined_data_returns_list(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertIsInstance(self.evefile.get_joined_data(), list)

    def test_get_joined_data_returns_data_objects(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        result = self.evefile.get_joined_data()
        self.assertTrue(result)
        for item in result:
            self.assertIsInstance(item, evefile.entities.data.MeasureData)

    def test_get_joined_data_by_default_returns_all_data_objects(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        result = self.evefile.get_joined_data()
        self.assertTrue(result)
        self.assertEqual(len(self.evefile.data), len(result))

    def test_get_joined_data_joins_data(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        result = self.evefile.get_joined_data()
        positions = np.union1d(
            self.evefile.data["SimMot:01"].position_counts,
            self.evefile.data["SimChan:01"].position_counts,
        )
        for item in result:
            self.assertEqual(len(positions), len(item.position_counts))

    def test_get_joined_data_uses_correct_mode(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        result = self.evefile.get_joined_data(mode="AxisAndChannelPositions")
        positions = np.intersect1d(
            self.evefile.data["SimMot:01"].position_counts,
            self.evefile.data["SimChan:01"].position_counts,
        )
        for item in result:
            self.assertEqual(len(positions), len(item.position_counts))

    def test_get_joined_data_with_ids(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        ids = list(self.evefile.data)
        result = self.evefile.get_joined_data(data=ids)
        for item in result:
            self.assertIsInstance(item, evefile.entities.data.MeasureData)

    def test_get_joined_data_with_names(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        names = [item.metadata.name for item in self.evefile.data.values()]
        result = self.evefile.get_joined_data(data=names)
        for item in result:
            self.assertIsInstance(item, evefile.entities.data.MeasureData)

    def test_get_joined_data_with_monitor_ids(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        ids = list(self.evefile.data)
        ids.extend(list(self.evefile.monitors))
        result = self.evefile.get_joined_data(data=ids)
        self.assertEqual(3, len(result))
        for item in result:
            self.assertIsInstance(item, evefile.entities.data.MeasureData)

    def test_get_joined_data_with_monitors_true_includes_monitor_objects(
        self,
    ):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        result = self.evefile.get_joined_data(include_monitors=True)
        self.assertTrue(result)
        self.assertEqual(
            len(self.evefile.data) + len(self.evefile.monitors), len(result)
        )

    def test_show_info_prints_metadata(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        temp_stdout = StringIO()
        with contextlib.redirect_stdout(temp_stdout):
            self.evefile.show_info()
        output = temp_stdout.getvalue().strip()
        self.assertIn("METADATA", output)
        self.assertIn("filename: ", output)

    def test_show_info_prints_log_messages(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        log_message = evefile.entities.file.LogMessage()
        log_message.from_string("2025-08-12T09:06:05: Lorem ipsum")
        self.evefile.log_messages.append(log_message)
        temp_stdout = StringIO()
        with contextlib.redirect_stdout(temp_stdout):
            self.evefile.show_info()
        output = temp_stdout.getvalue().strip()
        self.assertIn("LOG MESSAGES", output)
        self.assertIn(": Lorem ipsum", output)

    def test_show_info_prints_data(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        temp_stdout = StringIO()
        with contextlib.redirect_stdout(temp_stdout):
            self.evefile.show_info()
        output = temp_stdout.getvalue().strip()
        self.assertIn(f"\nDATA", output)

    def test_show_info_prints_snapshots(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(set_preferred=True, add_snapshot=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        temp_stdout = StringIO()
        with contextlib.redirect_stdout(temp_stdout):
            self.evefile.show_info()
        output = temp_stdout.getvalue().strip()
        self.assertIn(f"\nSNAPSHOTS", output)

    def test_show_info_prints_monitors(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(set_preferred=True, add_snapshot=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        temp_stdout = StringIO()
        with contextlib.redirect_stdout(temp_stdout):
            self.evefile.show_info()
        output = temp_stdout.getvalue().strip()
        self.assertIn(f"\nMONITORS", output)
        # print(output)

    def test_get_dataframe_returns_dataframe(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(set_preferred=True, add_snapshot=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        dataframe = self.evefile.get_dataframe()
        self.assertIsInstance(dataframe, pd.DataFrame)

    def test_dataframe_by_default_contains_all_data_objects(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        dataframe = self.evefile.get_dataframe()
        self.assertTrue(dataframe.columns.size)
        self.assertListEqual(
            list(dataframe.columns),
            [item.metadata.name for item in self.evefile.data.values()],
        )

    def test_dataframe_with_name_of_data_object(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        data_name = "foo"
        dataframe = self.evefile.get_dataframe(data=[data_name])
        self.assertTrue(dataframe.columns.size)
        self.assertListEqual(list(dataframe.columns), [data_name])

    def test_dataframe_with_id_of_data_object(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        data_name = "SimMot:01"
        dataframe = self.evefile.get_dataframe(data=[data_name])
        self.assertTrue(dataframe.columns.size)
        self.assertListEqual(
            list(dataframe.columns),
            [self.evefile.data[data_name].metadata.name],
        )

    def test_dataframe_contains_index_name(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        dataframe = self.evefile.get_dataframe()
        self.assertEqual("position", dataframe.index.name)

    def test_dataframe_returns_positions_as_index(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        dataframe = self.evefile.get_dataframe()
        self.assertGreater(dataframe.index[0], 0)

    def test_get_dataframe_uses_correct_mode(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        dataframe = self.evefile.get_dataframe(mode="AxisAndChannelPositions")
        positions = np.intersect1d(
            self.evefile.data["SimMot:01"].position_counts,
            self.evefile.data["SimChan:01"].position_counts,
        )
        self.assertEqual(len(positions), len(dataframe.index))

    def test_dataframe_with_monitors_true_includes_monitor_objects(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        dataframe = self.evefile.get_dataframe(include_monitors=True)
        column_names = [
            item.metadata.name for item in self.evefile.data.values()
        ]
        column_names.extend(list(self.evefile.monitors))
        self.assertListEqual(
            list(dataframe.columns),
            column_names,
        )

    def test_get_monitor_returns_device_data(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        monitor_name = list(self.evefile.monitors.keys())[0]
        self.assertIsInstance(
            self.evefile.get_monitors(monitor_name),
            evefile.entities.data.DeviceData,
        )

    def test_get_monitor_list_returns_device_data_as_array(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        monitor_name = list(self.evefile.monitors.keys())[0]
        device_data = self.evefile.get_monitors([monitor_name, monitor_name])
        self.assertIsInstance(device_data, list)
        for item in device_data:
            self.assertIsInstance(item, evefile.entities.data.DeviceData)

    def test_get_monitors_by_default_returns_all_mapped_monitors(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create()
        self.evefile = evefile.EveFile(filename=self.filename)
        result = self.evefile.get_monitors()
        self.assertTrue(result)
        if len(self.evefile.monitors) > 2:
            self.assertEqual(len(self.evefile.monitors), len(result))
            for item in result:
                self.assertIsInstance(item, evefile.entities.data.DeviceData)
        else:
            self.assertIsInstance(result, evefile.entities.data.DeviceData)

    def test_get_snapshots_returns_pandas_dataframe(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(add_snapshot=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        self.assertIsInstance(
            self.evefile.get_snapshots(),
            pd.DataFrame,
        )

    def test_get_snapshots_returns_df_with_snapshot_names_as_index(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(add_snapshot=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        snapshot_df = self.evefile.get_snapshots()
        snapshot_names = [
            item.metadata.name for item in self.evefile.snapshots.values()
        ]
        self.assertListEqual(snapshot_names, snapshot_df.index.to_list())

    def test_get_snapshots_returns_df_with_poscounts_as_columns(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(add_snapshot=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        snapshot_df = self.evefile.get_snapshots()
        self.assertListEqual([1, 9], snapshot_df.columns.to_list())

    def test_get_snapshots_returns_df_with_values_as_rows(self):
        h5file = DummyHDF5File(filename=self.filename)
        h5file.create(add_snapshot=True)
        self.evefile = evefile.EveFile(filename=self.filename)
        snapshot_df = self.evefile.get_snapshots()
        snapshot_names = list(self.evefile.snapshots.keys())
        np.testing.assert_array_equal(
            self.evefile.snapshots[snapshot_names[0]].data,
            snapshot_df.iloc[0:].values[0],
        )
