"""Tests for the utils module of the mlfastflow package."""

import os
import tempfile
import unittest
from pathlib import Path

import pandas as pd
import polars as pl

from mlfastflow.utils import (
    configure_logging,
    timer_decorator,
    concat_files,
    profile,
    csv2parquet,
    parquet2csv,
    get_info,
)


class TestTimerDecorator(unittest.TestCase):
    """Test cases for the timer_decorator function."""

    def test_timer_decorator_basic(self):
        """Test that timer decorator works and logs timing."""
        @timer_decorator
        def test_func():
            return "test_result"

        with self.assertLogs("mlfastflow.utils", level="INFO") as cm:
            result = test_func()

        self.assertEqual(result, "test_result")
        self.assertEqual(len(cm.output), 1)
        self.assertIn("test_func", cm.output[0])
        self.assertIn("Finished", cm.output[0])

    def test_timer_decorator_with_exception(self):
        """Test that timer decorator still logs timing even when function raises."""
        @timer_decorator
        def test_func():
            raise ValueError("test error")

        with self.assertLogs("mlfastflow.utils", level="INFO") as cm:
            with self.assertRaises(ValueError):
                test_func()

        self.assertEqual(len(cm.output), 1)
        self.assertIn("test_func", cm.output[0])
        self.assertIn("Finished", cm.output[0])


class TestConcatFiles(unittest.TestCase):
    """Test cases for the concat_files function."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()
        self.temp_path = Path(self.temp_dir)
        
        # Create test CSV files
        self.csv1_path = self.temp_path / "test1.csv"
        self.csv2_path = self.temp_path / "test2.csv"
        
        # Create test data
        df1 = pl.DataFrame({"a": [1, 2], "b": ["x", "y"]})
        df2 = pl.DataFrame({"a": [3, 4], "b": ["z", "w"]})
        
        df1.write_csv(self.csv1_path)
        df2.write_csv(self.csv2_path)

    def tearDown(self):
        """Clean up test fixtures."""
        import shutil
        shutil.rmtree(self.temp_dir)

    def test_concat_csv_files(self):
        """Test concatenating CSV files."""
        result_path = concat_files(self.temp_path, file_type='csv')
        
        self.assertIsNotNone(result_path)
        result_file = Path(result_path)
        self.assertTrue(result_file.exists())
        
        # Check the concatenated content
        result_df = pl.read_csv(result_file)
        self.assertEqual(len(result_df), 4)  # 2 + 2 rows

    def test_concat_with_source_column(self):
        """Test concatenating files with source column."""
        result_path = concat_files(self.temp_path, file_type='csv', add_source_column=True)
        
        self.assertIsNotNone(result_path)
        result_df = pl.read_csv(result_path)
        self.assertIn("SOURCE", result_df.columns)

    def test_concat_nonexistent_folder(self):
        """Test concat_files with nonexistent folder."""
        nonexistent_path = self.temp_path / "nonexistent"
        
        with self.assertRaises(FileNotFoundError):
            concat_files(nonexistent_path)

    def test_concat_unsupported_file_type(self):
        """Test concat_files with unsupported file type."""
        with self.assertRaises(ValueError):
            concat_files(self.temp_path, file_type='xlsx')

    def test_concat_no_files_found(self):
        """Test concat_files when no files of specified type are found."""
        # Create empty directory
        empty_dir = self.temp_path / "empty"
        empty_dir.mkdir()

        with self.assertRaises(FileNotFoundError):
            concat_files(empty_dir, file_type='csv')


try:
    import ydata_profiling as _ydata  # noqa: F401
    _HAS_YDATA = True
except ImportError:
    _HAS_YDATA = False


@unittest.skipUnless(_HAS_YDATA, "ydata-profiling not installed")
class TestProfile(unittest.TestCase):
    """Test cases for the profile function."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()
        self.temp_path = Path(self.temp_dir)
        
        # Create test DataFrame
        self.test_df = pd.DataFrame({
            'numeric': [1, 2, 3, 4, 5],
            'text': ['a', 'b', 'c', 'd', 'e'],
            'float': [1.1, 2.2, 3.3, 4.4, 5.5]
        })

    def tearDown(self):
        """Clean up test fixtures."""
        import shutil
        shutil.rmtree(self.temp_dir)

    def test_profile_pandas_dataframe(self):
        """Test profiling a pandas DataFrame."""
        result = profile(self.test_df, output_path=str(self.temp_path))
        
        self.assertIsNotNone(result)
        # Check that HTML file was created
        html_files = list(self.temp_path.glob("*.html"))
        self.assertEqual(len(html_files), 1)

    def test_profile_polars_dataframe(self):
        """Test profiling a polars DataFrame."""
        polars_df = pl.from_pandas(self.test_df)
        result = profile(polars_df, output_path=str(self.temp_path))
        
        self.assertIsNotNone(result)
        # Check that HTML file was created
        html_files = list(self.temp_path.glob("*.html"))
        self.assertEqual(len(html_files), 1)

    def test_profile_empty_dataframe(self):
        """Test profiling an empty DataFrame."""
        empty_df = pd.DataFrame()
        
        with self.assertRaises(ValueError):
            profile(empty_df)

    def test_profile_invalid_input(self):
        """Test profiling with invalid input."""
        with self.assertRaises(TypeError):
            profile("not_a_dataframe")

    def test_profile_custom_title(self):
        """Test profiling with custom title."""
        custom_title = "My Custom Report"
        result = profile(self.test_df, title=custom_title, output_path=str(self.temp_path))
        
        self.assertIsNotNone(result)
        # Check that file with custom title was created
        expected_filename = "My_Custom_Report.html"
        expected_path = self.temp_path / expected_filename
        self.assertTrue(expected_path.exists())


class TestCsv2Parquet(unittest.TestCase):
    """Test cases for the csv2parquet function."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()
        self.temp_path = Path(self.temp_dir)
        
        # Create test CSV file
        self.csv_file = self.temp_path / "test.csv"
        test_df = pl.DataFrame({
            "id": [1, 2, 3],
            "name": ["Alice", "Bob", "Charlie"],
            "value": [10.5, 20.3, 30.1]
        })
        test_df.write_csv(self.csv_file)

    def tearDown(self):
        """Clean up test fixtures."""
        import shutil
        shutil.rmtree(self.temp_dir)

    def test_convert_single_file(self):
        """Test converting a single CSV file to Parquet."""
        result = csv2parquet(self.csv_file)
        
        self.assertEqual(len(result), 1)
        parquet_file = Path(result[0])
        self.assertTrue(parquet_file.exists())
        self.assertEqual(parquet_file.suffix, '.parquet')
        
        # Verify content
        df = pl.read_parquet(parquet_file)
        self.assertEqual(len(df), 3)
        self.assertEqual(df.columns, ["id", "name", "value"])

    def test_convert_directory(self):
        """Test converting all CSV files in a directory."""
        # Create another CSV file
        csv_file2 = self.temp_path / "test2.csv"
        test_df2 = pl.DataFrame({"x": [4, 5], "y": ["D", "E"]})
        test_df2.write_csv(csv_file2)
        
        result = csv2parquet(self.temp_path)
        
        self.assertEqual(len(result), 2)
        for file_path in result:
            self.assertTrue(Path(file_path).exists())
            self.assertEqual(Path(file_path).suffix, '.parquet')

    def test_convert_with_output_dir(self):
        """Test converting with custom output directory."""
        output_dir = self.temp_path / "output"
        result = csv2parquet(self.csv_file, output_dir=output_dir)
        
        self.assertEqual(len(result), 1)
        result_file = Path(result[0])
        self.assertTrue(result_file.exists())
        # Use resolve() to handle symlinks consistently
        self.assertEqual(result_file.parent.resolve(), output_dir.resolve())

    def test_convert_nonexistent_file(self):
        """Test converting nonexistent file."""
        nonexistent_file = self.temp_path / "nonexistent.csv"
        
        with self.assertRaises(FileNotFoundError):
            csv2parquet(nonexistent_file)

    def test_convert_non_csv_file(self):
        """Test converting non-CSV file."""
        txt_file = self.temp_path / "test.txt"
        txt_file.write_text("not a csv")
        
        result = csv2parquet(txt_file)
        self.assertEqual(len(result), 0)

    def test_convert_with_subdirectories(self):
        """Test converting CSV files in subdirectories."""
        # Create subdirectory with CSV file
        sub_dir = self.temp_path / "subdir"
        sub_dir.mkdir()
        csv_file_sub = sub_dir / "sub_test.csv"
        test_df_sub = pl.DataFrame({"a": [1], "b": ["test"]})
        test_df_sub.write_csv(csv_file_sub)
        
        result = csv2parquet(self.temp_path, sub_folders=True)
        
        # Should find both the main CSV and the subdirectory CSV
        self.assertEqual(len(result), 2)
        
        # Check that subdirectory structure is preserved
        sub_parquet_files = [f for f in result if "subdir" in f]
        self.assertEqual(len(sub_parquet_files), 1)


@unittest.skipUnless(_HAS_YDATA, "ydata-profiling not installed")
class TestUtilsIntegration(unittest.TestCase):
    """Integration tests for utils functions."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()
        self.temp_path = Path(self.temp_dir)

    def tearDown(self):
        """Clean up test fixtures."""
        import shutil
        shutil.rmtree(self.temp_dir)

    def test_csv_to_parquet_to_profile_workflow(self):
        """Test a complete workflow: CSV -> Parquet -> Profile."""
        # Create CSV file
        csv_file = self.temp_path / "workflow_test.csv"
        df = pl.DataFrame({
            "id": range(100),
            "category": ["A", "B", "C"] * 33 + ["A"],
            "value": [i * 0.1 for i in range(100)]
        })
        df.write_csv(csv_file)
        
        # Convert to Parquet
        parquet_files = csv2parquet(csv_file)
        self.assertEqual(len(parquet_files), 1)
        
        # Load Parquet and profile
        parquet_df = pl.read_parquet(parquet_files[0])
        profile_result = profile(parquet_df, output_path=str(self.temp_path))
        
        self.assertIsNotNone(profile_result)
        # Check that profile HTML was created
        html_files = list(self.temp_path.glob("*.html"))
        self.assertEqual(len(html_files), 1)


class TestConcatFilesOverwrite(unittest.TestCase):
    """Test the overwrite guard added to concat_files."""

    def setUp(self):
        self.temp_dir = tempfile.mkdtemp()
        self.temp_path = Path(self.temp_dir)
        df = pl.DataFrame({"a": [1, 2]})
        df.write_csv(self.temp_path / "a.csv")

    def tearDown(self):
        import shutil
        shutil.rmtree(self.temp_dir)

    def test_overwrite_false_raises_when_file_exists(self):
        """concat_files should raise FileExistsError when output exists and overwrite=False."""
        out = self.temp_path.parent / f"{self.temp_path.name}_combined.csv"
        out.write_text("existing")
        try:
            with self.assertRaises(FileExistsError):
                concat_files(self.temp_path, file_type="csv", overwrite=False)
        finally:
            out.unlink(missing_ok=True)

    def test_overwrite_true_replaces_file(self):
        """concat_files should replace an existing output when overwrite=True."""
        out = self.temp_path.parent / f"{self.temp_path.name}_combined.csv"
        out.write_text("old content")
        try:
            result = concat_files(self.temp_path, file_type="csv", overwrite=True)
            self.assertTrue(Path(result).exists())
            self.assertNotEqual(Path(result).read_text().strip(), "old content")
        finally:
            out.unlink(missing_ok=True)


class TestParquet2Csv(unittest.TestCase):
    """Test cases for the parquet2csv function."""

    def setUp(self):
        self.temp_dir = tempfile.mkdtemp()
        self.temp_path = Path(self.temp_dir)

        self.pq_file = self.temp_path / "test.parquet"
        pl.DataFrame({"id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"]})\
            .write_parquet(self.pq_file)

    def tearDown(self):
        import shutil
        shutil.rmtree(self.temp_dir)

    def test_convert_single_file(self):
        result = parquet2csv(self.pq_file)
        self.assertEqual(len(result), 1)
        csv_file = Path(result[0])
        self.assertTrue(csv_file.exists())
        self.assertEqual(csv_file.suffix, ".csv")
        df = pl.read_csv(csv_file)
        self.assertEqual(len(df), 3)

    def test_convert_directory(self):
        pq2 = self.temp_path / "test2.parquet"
        pl.DataFrame({"x": [9, 8]}).write_parquet(pq2)
        result = parquet2csv(self.temp_path)
        self.assertEqual(len(result), 2)

    def test_convert_with_output_dir(self):
        out_dir = self.temp_path / "csv_out"
        result = parquet2csv(self.pq_file, output_dir=out_dir)
        self.assertEqual(len(result), 1)
        self.assertEqual(Path(result[0]).parent.resolve(), out_dir.resolve())

    def test_skip_non_parquet_file(self):
        txt = self.temp_path / "readme.txt"
        txt.write_text("hello")
        result = parquet2csv(txt)
        self.assertEqual(result, [])

    def test_overwrite_false_skips(self):
        csv_out = self.pq_file.with_suffix(".csv")
        csv_out.write_text("old")
        result = parquet2csv(self.pq_file, overwrite=False)
        self.assertEqual(result, [])
        self.assertEqual(csv_out.read_text(), "old")

    def test_overwrite_true_replaces(self):
        csv_out = self.pq_file.with_suffix(".csv")
        csv_out.write_text("old")
        result = parquet2csv(self.pq_file, overwrite=True)
        self.assertEqual(len(result), 1)
        self.assertNotEqual(Path(result[0]).read_text().strip(), "old")

    def test_nonexistent_path_raises(self):
        with self.assertRaises(FileNotFoundError):
            parquet2csv(self.temp_path / "nope.parquet")


class TestGetInfo(unittest.TestCase):
    """Test cases for the get_info function."""

    def test_pandas_dataframe(self):
        df = pd.DataFrame({"a": [1, 2, None], "b": ["x", "y", "z"]})
        info = get_info(df)
        self.assertEqual(info["rows"], 3)
        self.assertEqual(info["columns"], 2)
        self.assertIn("a", info["column_names"])
        self.assertEqual(info["null_counts"]["a"], 1)
        self.assertIsNotNone(info["memory_mb"])
        self.assertEqual(info["backend"], "pandas")

    def test_polars_dataframe(self):
        df = pl.DataFrame({"x": [10, 20], "y": [1.5, 2.5]})
        info = get_info(df)
        self.assertEqual(info["rows"], 2)
        self.assertEqual(info["columns"], 2)
        self.assertEqual(info["backend"], "polars")
        self.assertIsNotNone(info["memory_mb"])

    def test_polars_lazyframe(self):
        lf = pl.DataFrame({"a": [1, 2, 3]}).lazy()
        info = get_info(lf)
        self.assertIsNone(info["rows"])
        self.assertIsNone(info["null_counts"])
        self.assertIsNone(info["memory_mb"])
        self.assertEqual(info["backend"], "polars-lazy")

    def test_invalid_type_raises(self):
        with self.assertRaises(TypeError):
            get_info("not a dataframe")


class TestConfigureLogging(unittest.TestCase):
    """Test cases for configure_logging."""

    def test_configure_logging_runs_without_error(self):
        """configure_logging should not raise."""
        configure_logging()

    def test_configure_logging_with_debug(self):
        """configure_logging should accept string level names."""
        configure_logging("DEBUG")


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