"""Tests for logging format, pipeline summary, comparability metrics, and preparation summary."""

import io
import re

import numpy as np
import pandas as pd
from loguru import logger

from Capricho.logger import setup_logger


def _make_log_sink():
    """Create a loguru sink that captures all messages to a StringIO."""
    sink = io.StringIO()
    logger.remove()
    logger.add(sink, format="{message}", level="INFO", colorize=False)
    return sink


class TestTwoTierLogFormat:
    """Test that logger uses compact format for INFO+ and verbose for DEBUG."""

    def test_compact_format_has_no_timestamp(self):
        """INFO messages should not contain timestamps."""
        sink = io.StringIO()
        logger.remove()
        from Capricho.logger import COMPACT_FORMAT

        logger.add(sink, format=COMPACT_FORMAT, level="INFO", colorize=False)
        logger.info("Test message")
        output = sink.getvalue()
        # Should NOT contain timestamp pattern like "2026-02-13 14:32:17"
        assert not re.search(
            r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", output
        ), f"Compact format should not contain timestamps, got: {output!r}"

    def test_compact_format_has_no_source_location(self):
        """INFO messages should not contain module:function:line."""
        sink = io.StringIO()
        logger.remove()
        from Capricho.logger import COMPACT_FORMAT

        logger.add(sink, format=COMPACT_FORMAT, level="INFO", colorize=False)
        logger.info("Test message")
        output = sink.getvalue()
        # Should NOT contain source location pattern like "module:function:123"
        assert not re.search(
            r"\w+:\w+:\d+", output
        ), f"Compact format should not contain source location, got: {output!r}"

    def test_compact_format_contains_level_and_message(self):
        """INFO messages should contain the level and the message text."""
        sink = io.StringIO()
        logger.remove()
        from Capricho.logger import COMPACT_FORMAT

        logger.add(sink, format=COMPACT_FORMAT, level="INFO", colorize=False)
        logger.info("Pipeline complete")
        output = sink.getvalue()
        assert "INFO" in output
        assert "Pipeline complete" in output

    def test_verbose_format_has_timestamp(self):
        """DEBUG messages should contain timestamps."""
        sink = io.StringIO()
        logger.remove()
        from Capricho.logger import VERBOSE_FORMAT

        logger.add(sink, format=VERBOSE_FORMAT, level="DEBUG", colorize=False)
        logger.debug("Debug details")
        output = sink.getvalue()
        assert re.search(
            r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", output
        ), f"Verbose format should contain timestamps, got: {output!r}"

    def test_verbose_format_has_source_location(self):
        """DEBUG messages should contain module:function:line."""
        sink = io.StringIO()
        logger.remove()
        from Capricho.logger import VERBOSE_FORMAT

        logger.add(sink, format=VERBOSE_FORMAT, level="DEBUG", colorize=False)
        logger.debug("Debug details")
        output = sink.getvalue()
        # Should contain source location like "test_logging:test_verbose...:NN"
        assert re.search(
            r"\w+:\w+:\d+", output
        ), f"Verbose format should contain source location, got: {output!r}"

    def test_setup_logger_info_level_uses_compact(self):
        """When log level is INFO, output should use compact format (no timestamps)."""
        sink = io.StringIO()
        logger.remove()
        setup_logger(level="INFO", _sink=sink)
        logger.info("Info test")
        output = sink.getvalue()
        assert "Info test" in output
        assert not re.search(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", output)

    def test_setup_logger_debug_level_has_verbose_for_debug(self):
        """When log level is DEBUG, DEBUG messages should use verbose format."""
        sink = io.StringIO()
        logger.remove()
        setup_logger(level="DEBUG", _sink=sink)
        logger.debug("Debug test")
        output = sink.getvalue()
        assert "Debug test" in output
        assert re.search(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", output)

    def test_setup_logger_debug_level_info_still_compact(self):
        """When log level is DEBUG, INFO messages should still use compact format."""
        # We need two sinks to separate the formats
        compact_sink = io.StringIO()
        verbose_sink = io.StringIO()
        logger.remove()
        setup_logger(level="DEBUG", _sink=compact_sink, _verbose_sink=verbose_sink)
        logger.info("Info in debug mode")
        compact_output = compact_sink.getvalue()
        # The compact sink should capture this INFO message without timestamp
        assert "Info in debug mode" in compact_output
        assert not re.search(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", compact_output)


class TestPipelineSummary:
    """Test the pipeline summary report function."""

    def _make_pre_agg_df(self):
        """Create a realistic pre-aggregation DataFrame with flag columns."""
        return pd.DataFrame(
            {
                "activity_id": range(1, 11),
                "molecule_chembl_id": [f"CHEMBL{i}" for i in range(1, 11)],
                "target_chembl_id": ["CHEMBL203"] * 5 + ["CHEMBL204"] * 5,
                "assay_chembl_id": [f"CHEMBL_A{i}" for i in range(1, 11)],
                "pchembl_value": [6.5, 7.0, 5.5, 8.0, 6.0, 7.5, 5.0, 6.5, 7.0, 8.5],
                "connectivity": [f"CONN{i}" for i in range(1, 11)],
                "data_dropping_comment": [
                    "",
                    "Data Validity Comment Present",
                    "",
                    "Potential Duplicate",
                    "Undefined Stereochemistry",
                    "",
                    "Data Validity Comment Present & Potential Duplicate",
                    "",
                    "",
                    "Assay size < 20",
                ],
                "data_processing_comment": [
                    "",
                    "Calculated pChEMBL",
                    "Salt/solvent removed",
                    "",
                    "",
                    "Calculated pChEMBL",
                    "",
                    "Salt/solvent removed & Calculated pChEMBL",
                    "",
                    "",
                ],
            }
        )

    def test_summary_counts_flags_correctly(self):
        """Pipeline summary should count individual flags from & -separated comments."""
        from Capricho.cli.chembl_data_pipeline import _log_pipeline_summary

        sink = _make_log_sink()
        df = self._make_pre_agg_df()
        _log_pipeline_summary(df, pre_aggregation_count=10, post_aggregation_count=8)
        output = sink.getvalue()

        assert "PIPELINE SUMMARY" in output
        assert "Rows fetched:" in output
        assert "10" in output  # pre_aggregation_count

    def test_summary_counts_dropping_flags(self):
        """Should count each dropping flag individually, including & -separated ones."""
        from Capricho.cli.chembl_data_pipeline import _log_pipeline_summary

        sink = _make_log_sink()
        df = self._make_pre_agg_df()
        _log_pipeline_summary(df, pre_aggregation_count=10, post_aggregation_count=8)
        output = sink.getvalue()

        # "Data Validity Comment Present" appears in rows 2 and 7 -> count = 2
        assert "Data Validity Comment Present" in output
        # "Potential Duplicate" appears in rows 4 and 7 -> count = 2
        assert "Potential Duplicate" in output
        # "Undefined Stereochemistry" in row 5 -> count = 1
        assert "Undefined Stereochemistry" in output
        # "Assay size <" in row 10 -> count = 1
        assert "Assay size <" in output

    def test_summary_counts_processing_flags(self):
        """Should count processing flags."""
        from Capricho.cli.chembl_data_pipeline import _log_pipeline_summary

        sink = _make_log_sink()
        df = self._make_pre_agg_df()
        _log_pipeline_summary(df, pre_aggregation_count=10, post_aggregation_count=8)
        output = sink.getvalue()

        # "Calculated pChEMBL" in rows 2, 6, 8 -> count = 3
        assert "Calculated pChEMBL" in output
        # "Salt/solvent removed" in rows 3, 8 -> count = 2
        assert "Salt/solvent removed" in output

    def test_summary_shows_unique_counts(self):
        """Should show unique compounds, targets, and assays."""
        from Capricho.cli.chembl_data_pipeline import _log_pipeline_summary

        sink = _make_log_sink()
        df = self._make_pre_agg_df()
        _log_pipeline_summary(df, pre_aggregation_count=10, post_aggregation_count=8)
        output = sink.getvalue()

        assert "Unique compounds:" in output
        assert "Unique targets:" in output
        assert "Unique assays:" in output

    def test_summary_empty_df(self):
        """Should handle empty DataFrames gracefully."""
        from Capricho.cli.chembl_data_pipeline import _log_pipeline_summary

        sink = _make_log_sink()
        df = pd.DataFrame(
            columns=[
                "activity_id",
                "molecule_chembl_id",
                "target_chembl_id",
                "assay_chembl_id",
                "pchembl_value",
                "connectivity",
                "data_dropping_comment",
                "data_processing_comment",
            ]
        )
        _log_pipeline_summary(df, pre_aggregation_count=0, post_aggregation_count=0)
        output = sink.getvalue()
        assert "PIPELINE SUMMARY" in output

    def test_summary_no_flags(self):
        """Should handle DataFrames with no flags."""
        from Capricho.cli.chembl_data_pipeline import _log_pipeline_summary

        sink = _make_log_sink()
        df = pd.DataFrame(
            {
                "activity_id": [1, 2, 3],
                "molecule_chembl_id": ["CHEMBL1", "CHEMBL2", "CHEMBL3"],
                "target_chembl_id": ["CHEMBL203"] * 3,
                "assay_chembl_id": ["CHEMBL_A1"] * 3,
                "pchembl_value": [6.5, 7.0, 5.5],
                "connectivity": ["C1", "C2", "C3"],
                "data_dropping_comment": ["", "", ""],
                "data_processing_comment": ["", "", ""],
            }
        )
        _log_pipeline_summary(df, pre_aggregation_count=3, post_aggregation_count=3)
        output = sink.getvalue()
        assert "PIPELINE SUMMARY" in output
        # Should indicate no flags
        assert "(none)" in output.lower() or "No quality flags" in output


class TestComparabilityMetrics:
    """Test comparability metrics logging."""

    def test_metrics_logged_for_log_scale(self):
        """Metrics (±0.3, ±1.0 counts) should be logged for pchembl_value data."""
        from Capricho.analysis import _log_comparability_metrics

        sink = _make_log_sink()

        xp = np.array([5.0, 6.0, 7.0, 8.0, 9.0])
        yp = np.array([5.1, 6.2, 7.5, 8.0, 7.5])  # varying agreement

        _log_comparability_metrics(xp, yp, label="Test", is_log_scale=True)
        output = sink.getvalue()

        assert "0.3" in output
        assert "1.0" in output
        assert "Spearman" in output or "rho" in output

    def test_metrics_not_logged_for_linear_scale(self):
        """±0.3/±1.0 thresholds should NOT be logged for linear-scale data."""
        from Capricho.analysis import _log_comparability_metrics

        sink = _make_log_sink()

        xp = np.array([10.0, 50.0, 80.0])
        yp = np.array([15.0, 45.0, 85.0])

        _log_comparability_metrics(xp, yp, label="Percent", is_log_scale=False)
        output = sink.getvalue()

        # Should still log Spearman but not ±0.3/±1.0
        assert "Spearman" in output or "rho" in output
        assert "0.3" not in output
        assert "1.0 log" not in output

    def test_metrics_counts_within_thresholds(self):
        """Should correctly count pairs within ±0.3 and ±1.0."""
        from Capricho.analysis import _log_comparability_metrics

        sink = _make_log_sink()

        # 5 pairs: all within ±0.3 of identity
        xp = np.array([5.0, 6.0, 7.0, 8.0, 9.0])
        yp = np.array([5.1, 6.1, 7.1, 8.1, 9.1])

        _log_comparability_metrics(xp, yp, label="Good agreement", is_log_scale=True)
        output = sink.getvalue()

        # All 5 should be within ±0.3
        assert "100" in output or "5" in output  # 100% or count of 5

    def test_metrics_with_outliers(self):
        """Should detect outliers beyond ±1.0."""
        from Capricho.analysis import _log_comparability_metrics

        sink = _make_log_sink()

        xp = np.array([5.0, 6.0, 7.0])
        yp = np.array([5.0, 6.0, 9.5])  # last point is 2.5 log units off

        _log_comparability_metrics(xp, yp, label="Outlier test", is_log_scale=True)
        output = sink.getvalue()

        # Should report at least one point outside ±1.0
        assert "1.0" in output


class TestPreparationSummary:
    """Test the preparation summary logging."""

    def test_summary_logged_after_clean(self):
        """clean_data should log a summary with row counts."""
        from Capricho.cli.prepare import clean_data

        sink = _make_log_sink()

        df = pd.DataFrame(
            {
                "connectivity": ["C1", "C2", "C3"],
                "smiles": ["CCO", "CCC", "CCCO"],
                "pchembl_value": ["6.5", "7.0", "5.5"],
                "pchembl_value_mean": [6.5, 7.0, 5.5],
                "pchembl_value_median": [6.5, 7.0, 5.5],
                "pchembl_value_std": [0.0, 0.0, 0.0],
                "pchembl_value_counts": [1, 1, 1],
                "data_dropping_comment": ["", "", "Unit Annotation Error"],
                "data_processing_comment": ["", "", ""],
            }
        )
        clean_data(df, drop_flags=["Unit Annotation Error"])
        output = sink.getvalue()

        assert "PREPARATION SUMMARY" in output
        assert "Input rows:" in output or "3" in output

    def test_summary_shows_deduplication(self):
        """Summary should show deduplication counts when enabled."""
        from Capricho.cli.prepare import clean_data

        sink = _make_log_sink()

        df = pd.DataFrame(
            {
                "connectivity": ["C1"],
                "smiles": ["CCO"],
                "pchembl_value": ["6.50|6.50|7.00"],
                "pchembl_value_mean": [6.67],
                "pchembl_value_median": [6.50],
                "pchembl_value_std": [0.29],
                "pchembl_value_counts": [3],
                "data_dropping_comment": ["||"],
                "data_processing_comment": ["||"],
            }
        )
        clean_data(df, deduplicate=True)
        output = sink.getvalue()

        assert "PREPARATION SUMMARY" in output
        assert "Deduplication" in output or "deduplication" in output
