#!/usr/bin/python3
# SPDX-License-Identifier: MIT

"""
This module contains unit tests for the s2idle tool in the amd-debug-tools package.
"""

import math
import os
import tempfile
import unittest
from datetime import datetime
from unittest.mock import patch
import pandas as pd
from markupsafe import Markup

from amd_debug.sleep_report import (
    remove_duplicates,
    format_gpio_as_str,
    format_irq_as_str,
    format_as_human,
    format_as_seconds,
    format_watts,
    format_percent,
    format_timedelta,
    parse_hw_sleep,
    SleepReport,
)

from amd_debug.wake import WakeGPIO, WakeIRQ


class TestSleepReportUtils(unittest.TestCase):
    """Unit tests for the sleep report utilities."""

    def test_remove_duplicates(self):
        """Test the remove_duplicates function."""
        self.assertEqual(remove_duplicates("1, 2, 2, 3"), [1, 2, 3])
        self.assertEqual(remove_duplicates("4 4 5 6"), [4, 5, 6])
        self.assertEqual(remove_duplicates(""), [])

    def test_format_gpio_as_str(self):
        """Test the format_gpio_as_str function."""
        self.assertEqual(format_gpio_as_str("1, 2, 2, 3"), "1, 2, 3")
        self.assertEqual(format_gpio_as_str("4 4 5 6"), "4, 5, 6")
        self.assertEqual(format_gpio_as_str(""), "")

    @patch("amd_debug.wake.read_file")
    @patch("os.path.exists")
    @patch("os.listdir")
    @patch("os.walk")
    def test_format_irq_as_str(
        self, _mock_os_walk, _mock_os_listdir, mock_os_path_exists, mock_read_file
    ):
        """Test the format_irq_as_str function."""
        mock_read_file.side_effect = lambda path: {
            "/sys/kernel/irq/20/chip_name": "",
            "/sys/kernel/irq/20/actions": "",
            "/sys/kernel/irq/20/wakeup": "disabled",
        }.get(path, "")

        # Mocking os.path.exists
        mock_os_path_exists.return_value = False

        self.assertEqual(format_irq_as_str("20"), "Disabled interrupt")
        self.assertEqual(format_irq_as_str(""), "")

    def test_format_as_human(self):
        """Test the format_as_human function."""
        self.assertEqual(
            format_as_human("20231010123045"),
            datetime(2023, 10, 10, 12, 30, 45),
        )
        with self.assertRaises(ValueError):
            format_as_human("invalid_date")

    def test_format_as_seconds(self):
        """Test the format_as_seconds function."""
        self.assertEqual(
            format_as_seconds("20231010123045"),
            datetime(2023, 10, 10, 12, 30, 45).timestamp(),
        )
        with self.assertRaises(ValueError):
            format_as_seconds("invalid_date")

    def test_format_watts(self):
        """Test the format_watts function."""
        self.assertEqual(format_watts(12.3456), "12.35W")
        self.assertEqual(format_watts(0), "0.00W")

    def test_format_percent(self):
        """Test the format_percent function."""
        self.assertEqual(format_percent(12.3456), "12.35%")
        self.assertEqual(format_percent(0), "0.00%")

    def test_format_timedelta(self):
        """Test the format_timedelta function."""
        self.assertEqual(format_timedelta(3600), "1:00:00")
        self.assertEqual(format_timedelta(3661), "1:01:01")

    def test_parse_hw_sleep(self):
        """Test the parse_hw_sleep function."""
        self.assertEqual(parse_hw_sleep(0.5), 50)
        self.assertEqual(parse_hw_sleep(1.0), 100)
        self.assertEqual(parse_hw_sleep(1.5), 0)


class TestSleepReport(unittest.TestCase):
    """Unit tests for the SleepReport class."""

    @patch("amd_debug.sleep_report.SleepDatabase")
    def setUp(self, MockSleepDatabase):
        """Set up a mock SleepReport instance for testing."""
        self.mock_db = MockSleepDatabase.return_value
        self.mock_db.report_summary_dataframe.return_value = pd.DataFrame(
            {
                "t0": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")],
                "t1": [datetime(2023, 10, 10, 12, 30, 0).strftime("%Y%m%d%H%M%S")],
                "hw": [50],
                "requested": [1],
                "gpio": ["1, 2"],
                "wake_irq": ["1"],
                "b0": [90],
                "b1": [85],
                "full": [100],
            }
        )
        self.since = datetime(2023, 10, 9, 0, 0, 0)
        self.until = datetime(2023, 10, 12, 0, 0, 0)
        self.report = SleepReport(
            since=self.since,
            until=self.until,
            fname=None,
            fmt="txt",
            tool_debug=False,
            report_debug=False,
        )

    def test_analyze_duration(self):
        """Test the analyze_duration method."""
        self.report.analyze_duration(
            index=0,
            t0=datetime(2023, 10, 10, 12, 0, 0),
            t1=datetime(2023, 10, 10, 12, 30, 0),
            requested=20,
            hw=50,
        )
        self.assertEqual(len(self.report.failures), 2)

    @patch("amd_debug.sleep_report.Environment")
    @patch("amd_debug.sleep_report.FileSystemLoader")
    def test_build_template(self, _mock_fsl, mock_env):
        """Test the build_template method."""
        mock_template = mock_env.return_value.get_template.return_value
        mock_template.render.return_value = "Rendered Template"
        result = self.report.build_template(inc_prereq=False)
        self.assertEqual(result, "Rendered Template")

    @patch("amd_debug.sleep_report.Environment")
    @patch("amd_debug.sleep_report.FileSystemLoader")
    def test_build_template_fchown_called_while_fd_open(self, _mock_fsl, mock_env):
        """Test that fchown is called before the file descriptor is closed.

        When SUDO_UID/SUDO_GID are set, os.fchown must be called inside the
        'with os.fdopen(...)' block so that the file descriptor is still valid.
        Calling it after the block exits causes OSError (Bad file descriptor).
        """
        mock_template = mock_env.return_value.get_template.return_value
        mock_template.render.return_value = "Rendered Template"

        with tempfile.TemporaryDirectory() as tmpdir:
            fname = os.path.join(tmpdir, "report.txt")
            self.report.fname = fname

            with patch.dict("os.environ", {"SUDO_UID": "1000", "SUDO_GID": "1000"}):
                with patch("os.fchown") as mock_fchown:
                    self.report.build_template(inc_prereq=False)
                    # fchown must have been called exactly once with the real fd
                    mock_fchown.assert_called_once()
                    _fd, uid, gid = mock_fchown.call_args[0]
                    self.assertEqual(uid, 1000)
                    self.assertEqual(gid, 1000)

    @patch("matplotlib.pyplot.savefig")
    def test_build_battery_chart(self, mock_savefig):
        """Test the build_battery_chart method."""
        self.report.build_battery_chart()
        self.assertIsNotNone(self.report.battery_svg)
        mock_savefig.assert_called_once()

    @patch("matplotlib.pyplot.savefig")
    def test_build_hw_sleep_chart(self, mock_savefig):
        """Test the build_hw_sleep_chart method."""
        self.report.build_hw_sleep_chart()
        self.assertIsNotNone(self.report.hwsleep_svg)
        mock_savefig.assert_called_once()

    def test_pre_process_dataframe_zero_duration(self):
        """Test the pre_process_dataframe method when t0 and t1 are the same."""
        # Mock the dataframe with t0 and t1 being the same
        self.report.df = pd.DataFrame(
            {
                "t0": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")],
                "t1": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")],
                "hw": [50],
                "requested": [1],
                "gpio": ["1, 2"],
                "wake_irq": ["1"],
                "b0": [90],
                "b1": [85],
                "full": [100],
            }
        )

        # Call the method
        self.report.pre_process_dataframe()

        # Verify the dataframe was processed correctly
        self.assertTrue(
            self.report.df["Duration"].isna().iloc[0]
        )  # Duration should be NaN
        self.assertTrue(
            math.isnan(self.report.df["Hardware Sleep"].iloc[0])
        )  # Hardware Sleep should be NaN

    def test_battery_ave_rate(self):
        """Test the pre_process_dataframe Average Power calculation."""
        self.report.df = pd.DataFrame(
            {
                "t0": [datetime(2023, 10, 10, 12, 0, 0).strftime("%Y%m%d%H%M%S")],
                "t1": [datetime(2023, 10, 10, 13, 0, 0).strftime("%Y%m%d%H%M%S")],
                "hw": [50],
                "requested": [1],
                "gpio": ["1, 2"],
                "wake_irq": ["1"],
                "b0": [90000000],
                "b1": [85000000],
                "full": [100000000],
            }
        )
        self.report.pre_process_dataframe()
        batt_ave_rate = self.report.df["Average Power"].iloc[0]
        self.assertAlmostEqual(batt_ave_rate, -5, places=3)

    def test_get_prereq_data_preserves_markup_for_html_tables(self):
        """Ensure HTML prerequisite tables remain Markup and are not escaped."""
        self.report.format = "html"
        self.report.debug = True
        self.mock_db.get_last_prereq_ts.return_value = "20231010123045"
        self.mock_db.report_prereq.return_value = []
        table_text = "DMI|value\nfoo|bar"
        self.mock_db.report_debug.return_value = [(table_text, 6)]

        prereq, _t0, prereq_debug = self.report.get_prereq_data()

        self.assertEqual(prereq, [])
        self.assertEqual(len(prereq_debug), 1)
        self.assertIsInstance(prereq_debug[0]["data"], Markup)
        self.assertIn("<table", str(prereq_debug[0]["data"]))
        self.assertNotIn("&lt;table", str(prereq_debug[0]["data"]))
