import datetime as _dt
from datetime import datetime, timedelta, date

from bouquin import strings

from PySide6.QtCore import Qt, QPoint
from PySide6.QtWidgets import QLabel
from PySide6.QtTest import QTest
from PySide6.QtCore import QDate

from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog


class FakeStatsDB:
    """Minimal stub that returns a fixed stats payload."""

    def __init__(self):
        d1 = _dt.date(2024, 1, 1)
        d2 = _dt.date(2024, 1, 2)
        self.stats = (
            2,  # pages_with_content
            5,  # total_revisions
            "2024-01-02",  # page_most_revisions
            3,  # page_most_revisions_count
            {d1: 10, d2: 20},  # words_by_date
            30,  # total_words
            4,  # unique_tags
            "2024-01-02",  # page_most_tags
            2,  # page_most_tags_count
            {d1: 1, d2: 2},  # revisions_by_date
        )
        self.called = False

    def gather_stats(self):
        self.called = True
        return self.stats


def test_statistics_dialog_populates_fields_and_heatmap(qtbot):
    # Make sure we have a known language for label texts
    strings.load_strings("en")

    db = FakeStatsDB()
    dlg = StatisticsDialog(db)
    qtbot.addWidget(dlg)
    dlg.show()

    # Stats were actually requested from the DB
    assert db.called

    # Window title comes from translations
    assert dlg.windowTitle() == strings._("statistics")

    # Grab all label texts for simple content checks
    label_texts = {lbl.text() for lbl in dlg.findChildren(QLabel)}

    # Page with most revisions / tags are rendered as "DATE (COUNT)"
    assert "2024-01-02 (3)" in label_texts
    assert "2024-01-02 (2)" in label_texts

    # Heatmap is created and uses "words" by default
    words_by_date = db.stats[4]
    revisions_by_date = db.stats[-1]

    assert hasattr(dlg, "_heatmap")
    assert dlg._heatmap._data == words_by_date

    # Switching the metric to "revisions" should swap the dataset
    dlg.metric_combo.setCurrentIndex(1)  # 0 = words, 1 = revisions
    qtbot.wait(10)
    assert dlg._heatmap._data == revisions_by_date


class EmptyStatsDB:
    """Stub that returns a 'no data yet' stats payload."""

    def __init__(self):
        self.called = False

    def gather_stats(self):
        self.called = True
        return (
            0,  # pages_with_content
            0,  # total_revisions
            None,  # page_most_revisions
            0,
            {},  # words_by_date
            0,  # total_words
            0,  # unique_tags
            None,  # page_most_tags
            0,
            {},  # revisions_by_date
        )


def test_statistics_dialog_no_data_shows_placeholder(qtbot):
    strings.load_strings("en")

    db = EmptyStatsDB()
    dlg = StatisticsDialog(db)
    qtbot.addWidget(dlg)
    dlg.show()

    assert db.called

    label_texts = [lbl.text() for lbl in dlg.findChildren(QLabel)]
    assert strings._("stats_no_data") in label_texts

    # When there's no data, the heatmap and metric combo shouldn't exist
    assert not hasattr(dlg, "metric_combo")
    assert not hasattr(dlg, "_heatmap")


def _date(year, month, day):
    return date(year, month, day)


# ============================================================================
# DateHeatmapTests - Missing Coverage
# ============================================================================


def test_activity_heatmap_empty_data(qtbot):
    """Test heatmap with empty data dict."""
    strings.load_strings("en")
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    # Set empty data
    heatmap.set_data({})

    # Should handle empty data gracefully
    assert heatmap._start is None
    assert heatmap._end is None
    assert heatmap._max_value == 0

    # Size hint should return default dimensions
    size = heatmap.sizeHint()
    assert size.width() > 0
    assert size.height() > 0

    # Paint should not crash
    heatmap.update()
    qtbot.wait(10)


def test_activity_heatmap_none_data(qtbot):
    """Test heatmap with None data."""
    strings.load_strings("en")
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    # Set None data
    heatmap.set_data(None)

    assert heatmap._start is None
    assert heatmap._end is None

    # Paint event should return early
    heatmap.update()
    qtbot.wait(10)


def test_activity_heatmap_click_when_no_data(qtbot):
    """Test clicking heatmap when there's no data."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    heatmap.set_data({})

    # Simulate click - should not crash or emit signal
    clicked_dates = []
    heatmap.date_clicked.connect(clicked_dates.append)

    # Click in the middle of widget
    pos = QPoint(100, 100)
    QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)

    # Should not have clicked any date
    assert len(clicked_dates) == 0


def test_activity_heatmap_click_outside_grid(qtbot):
    """Test clicking outside the grid area."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    # Set some data
    data = {
        date(2024, 1, 1): 5,
        date(2024, 1, 2): 10,
    }
    heatmap.set_data(data)

    clicked_dates = []
    heatmap.date_clicked.connect(clicked_dates.append)

    # Click in top-left margin (before grid starts)
    pos = QPoint(5, 5)
    QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)

    assert len(clicked_dates) == 0


def test_activity_heatmap_click_beyond_end_date(qtbot):
    """Test clicking on trailing empty cells beyond the last date."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    # Set data that doesn't fill a complete week
    data = {
        date(2024, 1, 1): 5,  # Monday
        date(2024, 1, 2): 10,  # Tuesday
    }
    heatmap.set_data(data)

    clicked_dates = []
    heatmap.date_clicked.connect(clicked_dates.append)

    # Try clicking far to the right (beyond end date)
    # This is tricky to target precisely, but we can simulate
    pos = QPoint(1000, 50)  # Far right
    QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)

    # Should either not click or only click valid dates
    # If it did click, it should be a valid date within range
    if clicked_dates:
        assert clicked_dates[0] <= date(2024, 1, 2)


def test_activity_heatmap_click_invalid_row(qtbot):
    """Test clicking below the 7 weekday rows."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    data = {
        date(2024, 1, 1): 5,
        date(2024, 1, 8): 10,
    }
    heatmap.set_data(data)

    clicked_dates = []
    heatmap.date_clicked.connect(clicked_dates.append)

    # Click below the grid (row 8 or higher)
    pos = QPoint(100, 500)  # Very low Y
    QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)

    assert len(clicked_dates) == 0


def test_activity_heatmap_right_click_ignored(qtbot):
    """Test that right-click is ignored."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    data = {date(2024, 1, 1): 5}
    heatmap.set_data(data)

    clicked_dates = []
    heatmap.date_clicked.connect(clicked_dates.append)

    # Right click should be ignored
    pos = QPoint(100, 100)
    QTest.mouseClick(heatmap, Qt.RightButton, pos=pos)

    assert len(clicked_dates) == 0


def test_activity_heatmap_month_label_rendering(qtbot):
    """Test heatmap spanning multiple months renders month labels."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    # Data spanning multiple months
    data = {
        date(2024, 1, 1): 5,
        date(2024, 1, 15): 10,
        date(2024, 2, 1): 8,
        date(2024, 2, 15): 12,
        date(2024, 3, 1): 6,
    }
    heatmap.set_data(data)

    # Should calculate proper size
    size = heatmap.sizeHint()
    assert size.width() > 0
    assert size.height() > 0

    # Paint should work without crashing
    heatmap.update()
    qtbot.wait(10)


def test_activity_heatmap_same_month_continues(qtbot):
    """Test that month labels skip weeks in the same month."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    # Multiple dates in same month
    data = {}
    for day in range(1, 29):  # January 1-28
        data[date(2024, 1, day)] = day

    heatmap.set_data(data)

    # Should render without issues
    heatmap.update()
    qtbot.wait(10)


def test_activity_heatmap_data_with_zero_values(qtbot):
    """Test heatmap with zero values in data."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    data = {
        date(2024, 1, 1): 0,
        date(2024, 1, 2): 5,
        date(2024, 1, 3): 0,
    }
    heatmap.set_data(data)

    assert heatmap._max_value == 5

    heatmap.update()
    qtbot.wait(10)


def test_activity_heatmap_single_day(qtbot):
    """Test heatmap with just one day of data."""
    heatmap = DateHeatmap()
    qtbot.addWidget(heatmap)
    heatmap.show()

    data = {date(2024, 1, 15): 10}
    heatmap.set_data(data)

    # Should handle single day
    assert heatmap._start is not None
    assert heatmap._end is not None

    clicked_dates = []
    heatmap.date_clicked.connect(clicked_dates.append)

    # Click should work
    pos = QPoint(100, 100)
    QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)


# ============================================================================
# StatisticsDialog Tests
# ============================================================================


def test_statistics_dialog_with_empty_database(qtbot, fresh_db):
    """Test statistics dialog with an empty database."""
    strings.load_strings("en")

    dialog = StatisticsDialog(fresh_db)
    qtbot.addWidget(dialog)
    dialog.show()

    # Should handle empty database gracefully
    assert dialog.isVisible()

    # Heatmap should be empty
    heatmap = dialog.findChild(DateHeatmap)
    if heatmap:
        # No crash when displaying empty heatmap
        qtbot.wait(10)


def test_statistics_dialog_with_data(qtbot, fresh_db):
    """Test statistics dialog with actual data."""
    strings.load_strings("en")

    # Add some content
    fresh_db.save_new_version("2024-01-01", "Hello world", "test")
    fresh_db.save_new_version("2024-01-02", "More content here", "test")
    fresh_db.save_new_version("2024-01-03", "Even more text", "test")

    dialog = StatisticsDialog(fresh_db)
    qtbot.addWidget(dialog)
    dialog.show()

    # Should display statistics
    assert dialog.isVisible()
    qtbot.wait(10)


def test_statistics_dialog_gather_stats_exception_handling(
    qtbot, fresh_db, monkeypatch
):
    """Test that gather_stats handles exceptions gracefully."""
    strings.load_strings("en")

    # Make dates_with_content raise an exception
    def bad_dates_with_content():
        raise RuntimeError("Simulated DB error")

    monkeypatch.setattr(fresh_db, "dates_with_content", bad_dates_with_content)

    # Should still create dialog without crashing
    dialog = StatisticsDialog(fresh_db)
    qtbot.addWidget(dialog)
    dialog.show()

    # Should handle error gracefully
    assert dialog.isVisible()


def test_statistics_dialog_with_sparse_data(qtbot, tmp_db_cfg, fresh_db):
    """Test statistics dialog with sparse data"""
    # Add some entries on non-consecutive days
    dates = ["2024-01-01", "2024-01-05", "2024-01-10", "2024-01-20"]
    for _date in dates:
        content = "Word " * 100  # 100 words
        fresh_db.save_new_version(_date, content, "note")

    dialog = StatisticsDialog(fresh_db)
    qtbot.addWidget(dialog)

    # Should create without crashing
    assert dialog is not None


def test_statistics_dialog_with_empty_data(qtbot, tmp_db_cfg, fresh_db):
    """Test statistics dialog with no data"""
    dialog = StatisticsDialog(fresh_db)
    qtbot.addWidget(dialog)

    # Should handle empty data gracefully
    assert dialog is not None


def test_statistics_dialog_date_range_selection(qtbot, tmp_db_cfg, fresh_db):
    """Test changing metric in statistics dialog"""
    # Add some test data
    for i in range(10):
        date = QDate.currentDate().addDays(-i).toString("yyyy-MM-dd")
        fresh_db.save_new_version(date, f"Content for day {i}", "note")

    dialog = StatisticsDialog(fresh_db)
    qtbot.addWidget(dialog)

    # Change metric to revisions
    idx = dialog.metric_combo.findData("revisions")
    if idx >= 0:
        dialog.metric_combo.setCurrentIndex(idx)
        qtbot.wait(50)

    # Change back to words
    idx = dialog.metric_combo.findData("words")
    if idx >= 0:
        dialog.metric_combo.setCurrentIndex(idx)
        qtbot.wait(50)


def test_heatmap_with_varying_word_counts(qtbot):
    """Test heatmap color scaling with varying word counts"""
    today = datetime.now().date()
    start = today - timedelta(days=30)

    entries = {}
    # Create entries with varying word counts
    for i in range(31):
        date = start + timedelta(days=i)
        entries[date] = i * 50  # Increasing word counts

    heatmap = DateHeatmap()
    heatmap.set_data(entries)
    qtbot.addWidget(heatmap)
    heatmap.show()

    # Should paint without errors
    assert heatmap.isVisible()


def test_heatmap_single_day(qtbot):
    """Test heatmap with single day of data"""
    today = datetime.now().date()
    entries = {today: 500}

    heatmap = DateHeatmap()
    heatmap.set_data(entries)
    qtbot.addWidget(heatmap)
    heatmap.show()

    assert heatmap.isVisible()


def test_statistics_dialog_metric_changes(qtbot, tmp_db_cfg, fresh_db):
    """Test various metric selections"""
    # Add data spanning multiple months
    base_date = QDate.currentDate().addDays(-90)
    for i in range(90):
        date = base_date.addDays(i).toString("yyyy-MM-dd")
        fresh_db.save_new_version(date, f"Day {i} content with many words", "note")

    dialog = StatisticsDialog(fresh_db)
    qtbot.addWidget(dialog)

    # Test each metric option
    for i in range(dialog.metric_combo.count()):
        dialog.metric_combo.setCurrentIndex(i)
        qtbot.wait(50)
