#!/usr/bin/env python3
"""Tests for weekly-retro.py - TDD approach (RED first, then GREEN)"""

import json
import sys
import tempfile
from datetime import date, datetime
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# Add scripts directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))

import weekly_retro as wr

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def sample_tasks() -> dict:
    """Minimal task-timers.json tasks dict for testing."""
    return {
        "task-1.1": {
            "task_id": "task-1.1",
            "team_id": "dev1-team",
            "description": "Deep work task",
            "start_time": "2026-03-09T10:00:00",
            "end_time": "2026-03-09T10:45:00",
            "duration_seconds": 2700.0,
            "status": "completed",
        },
        "task-1.2": {
            "task_id": "task-1.2",
            "team_id": "dev1-team",
            "description": "Medium work task",
            "start_time": "2026-03-10T11:00:00",
            "end_time": "2026-03-10T11:20:00",
            "duration_seconds": 1200.0,
            "status": "completed",
        },
        "task-1.3": {
            "task_id": "task-1.3",
            "team_id": "dev1-team",
            "description": "Micro work task",
            "start_time": "2026-03-11T12:00:00",
            "end_time": "2026-03-11T12:05:00",
            "duration_seconds": 300.0,
            "status": "completed",
        },
        "task-1.4": {
            "task_id": "task-1.4",
            "team_id": "dev2-team",
            "description": "Dev2 task A",
            "start_time": "2026-03-09T14:00:00",
            "end_time": "2026-03-09T14:10:00",
            "duration_seconds": 600.0,
            "status": "completed",
        },
        "task-1.5": {
            "task_id": "task-1.5",
            "team_id": "dev2-team",
            "description": "Dev2 task B",
            "start_time": "2026-03-10T15:00:00",
            "end_time": "2026-03-10T15:25:00",
            "duration_seconds": 1500.0,
            "status": "completed",
        },
        "task-1.6": {
            "task_id": "task-1.6",
            "team_id": "dev2-team",
            "description": "Dev2 task C (not completed)",
            "start_time": "2026-03-11T16:00:00",
            "end_time": None,
            "duration_seconds": 0.0,
            "status": "running",
        },
        "task-1.7": {
            "task_id": "task-1.7",
            "team_id": "dev1-team",
            "description": "Deep work task 2",
            "start_time": "2026-03-12T09:00:00",
            "end_time": "2026-03-12T10:00:00",
            "duration_seconds": 3600.0,
            "status": "completed",
        },
    }


@pytest.fixture
def tasks_json_file(sample_tasks, tmp_path) -> Path:
    """Write sample tasks to a temp JSON file."""
    data = {"tasks": sample_tasks}
    p = tmp_path / "task-timers.json"
    p.write_text(json.dumps(data))
    return p


@pytest.fixture
def empty_tasks_json_file(tmp_path) -> Path:
    """task-timers.json with no tasks."""
    data = {"tasks": {}}
    p = tmp_path / "task-timers.json"
    p.write_text(json.dumps(data))
    return p


@pytest.fixture
def sample_git_log() -> str:
    """Simulated git log output (one line per commit: <hash> <date> <subject>)."""
    return (
        "abc1234 2026-03-09 feat: add new feature\n"
        "abc1235 2026-03-10 fix: resolve null pointer\n"
        "abc1236 2026-03-10 fix: another bug\n"
        "abc1237 2026-03-11 refactor: clean up code\n"
        "abc1238 2026-03-12 docs: update readme\n"
        "abc1239 2026-03-12 chore: bump version\n"
        "abc1240 2026-03-13 some random message\n"
        "abc1241 2026-03-13 feat: new endpoint\n"
    )


# ---------------------------------------------------------------------------
# 1. load_tasks
# ---------------------------------------------------------------------------


class TestLoadTasks:
    def test_loads_valid_file(self, tasks_json_file):
        tasks = wr.load_tasks(tasks_json_file)
        assert len(tasks) == 7

    def test_returns_empty_dict_when_file_missing(self, tmp_path):
        missing = tmp_path / "no-file.json"
        tasks = wr.load_tasks(missing)
        assert tasks == {}

    def test_returns_empty_dict_when_tasks_key_missing(self, tmp_path):
        p = tmp_path / "bad.json"
        p.write_text(json.dumps({"other": {}}))
        tasks = wr.load_tasks(p)
        assert tasks == {}

    def test_returns_empty_dict_on_invalid_json(self, tmp_path):
        p = tmp_path / "bad.json"
        p.write_text("NOT JSON{{")
        tasks = wr.load_tasks(p)
        assert tasks == {}

    def test_empty_tasks_returns_empty_dict(self, empty_tasks_json_file):
        tasks = wr.load_tasks(empty_tasks_json_file)
        assert tasks == {}


# ---------------------------------------------------------------------------
# 2. filter_tasks_by_week
# ---------------------------------------------------------------------------


class TestFilterTasksByWeek:
    def test_filters_to_correct_week(self, sample_tasks):
        # 2026-W11: 2026-03-09 to 2026-03-15
        result = wr.filter_tasks_by_week(sample_tasks, "2026-11")
        # All tasks in sample have start_time in 2026-W11
        assert len(result) > 0

    def test_excludes_tasks_outside_week(self, sample_tasks):
        # 2026-W09: 2026-03-02 to 2026-03-08  -- none of sample tasks fall here
        result = wr.filter_tasks_by_week(sample_tasks, "2026-09")
        assert len(result) == 0

    def test_includes_only_completed_when_filtered(self, sample_tasks):
        result = wr.filter_tasks_by_week(sample_tasks, "2026-11")
        for task in result.values():
            assert task["status"] == "completed"

    def test_empty_tasks_returns_empty(self):
        result = wr.filter_tasks_by_week({}, "2026-11")
        assert result == {}


# ---------------------------------------------------------------------------
# 3. classify_session
# ---------------------------------------------------------------------------


class TestClassifySession:
    def test_deep_above_1800(self):
        assert wr.classify_session(1801) == "deep"

    def test_deep_at_exactly_1801(self):
        assert wr.classify_session(1801) == "deep"

    def test_medium_at_1800(self):
        # boundary: 1800s = 30min exactly -> medium (not strictly >)
        assert wr.classify_session(1800) == "medium"

    def test_medium_at_600(self):
        # boundary: 600s = 10min exactly -> medium
        assert wr.classify_session(600) == "medium"

    def test_micro_below_600(self):
        assert wr.classify_session(599) == "micro"

    def test_micro_at_zero(self):
        assert wr.classify_session(0) == "micro"

    def test_deep_large_value(self):
        assert wr.classify_session(7200) == "deep"

    def test_medium_midrange(self):
        assert wr.classify_session(900) == "medium"


# ---------------------------------------------------------------------------
# 4. compute_team_metrics
# ---------------------------------------------------------------------------


class TestComputeTeamMetrics:
    def test_task_count_per_team(self, sample_tasks):
        # Filter completed tasks for 2026-W11
        completed = {k: v for k, v in sample_tasks.items() if v["status"] == "completed"}
        metrics = wr.compute_team_metrics(completed)
        assert metrics["dev1-team"]["task_count"] == 4
        assert metrics["dev2-team"]["task_count"] == 2

    def test_avg_duration_dev1(self, sample_tasks):
        completed = {k: v for k, v in sample_tasks.items() if v["status"] == "completed"}
        metrics = wr.compute_team_metrics(completed)
        # dev1: 2700 + 1200 + 300 + 3600 = 7800 / 4 = 1950
        assert metrics["dev1-team"]["avg_duration_seconds"] == pytest.approx(1950.0, rel=1e-3)

    def test_avg_duration_dev2(self, sample_tasks):
        completed = {k: v for k, v in sample_tasks.items() if v["status"] == "completed"}
        metrics = wr.compute_team_metrics(completed)
        # dev2: 600 + 1500 = 2100 / 2 = 1050
        assert metrics["dev2-team"]["avg_duration_seconds"] == pytest.approx(1050.0, rel=1e-3)

    def test_session_pattern_dev1(self, sample_tasks):
        completed = {k: v for k, v in sample_tasks.items() if v["status"] == "completed"}
        metrics = wr.compute_team_metrics(completed)
        sp = metrics["dev1-team"]["session_pattern"]
        # deep: 2700, 3600 -> 2
        # medium: 1200 -> 1
        # micro: 300 -> 1
        assert sp["deep"] == 2
        assert sp["medium"] == 1
        assert sp["micro"] == 1

    def test_session_pattern_dev2(self, sample_tasks):
        completed = {k: v for k, v in sample_tasks.items() if v["status"] == "completed"}
        metrics = wr.compute_team_metrics(completed)
        sp = metrics["dev2-team"]["session_pattern"]
        # 600 -> medium, 1500 -> medium
        assert sp["deep"] == 0
        assert sp["medium"] == 2
        assert sp["micro"] == 0

    def test_empty_tasks_returns_empty_dict(self):
        metrics = wr.compute_team_metrics({})
        assert metrics == {}

    def test_fix_pct_not_set_by_compute_team_metrics(self, sample_tasks):
        """fix_pct is set later when commit data is merged."""
        completed = {k: v for k, v in sample_tasks.items() if v["status"] == "completed"}
        metrics = wr.compute_team_metrics(completed)
        # fix_pct defaults to 0.0 if no commit data
        for team_data in metrics.values():
            assert "fix_pct" in team_data
            assert team_data["fix_pct"] == 0.0


# ---------------------------------------------------------------------------
# 5. parse_git_log
# ---------------------------------------------------------------------------


class TestParseGitLog:
    def test_total_commit_count(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        assert result["total"] == 8

    def test_feat_count(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        assert result["by_type"]["feat"] == 2

    def test_fix_count(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        assert result["by_type"]["fix"] == 2

    def test_refactor_count(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        assert result["by_type"]["refactor"] == 1

    def test_docs_count(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        assert result["by_type"]["docs"] == 1

    def test_chore_count(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        assert result["by_type"]["chore"] == 1

    def test_other_count(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        assert result["by_type"]["other"] == 1

    def test_empty_log_returns_zero_total(self):
        result = wr.parse_git_log("")
        assert result["total"] == 0
        assert result["by_type"]["feat"] == 0

    def test_all_types_present_in_by_type(self, sample_git_log):
        result = wr.parse_git_log(sample_git_log)
        for t in ["feat", "fix", "refactor", "docs", "chore", "other"]:
            assert t in result["by_type"]

    def test_feat_with_scope(self):
        log = "abc1234 2026-03-09 feat(api): add endpoint\n"
        result = wr.parse_git_log(log)
        assert result["by_type"]["feat"] == 1

    def test_case_insensitive_prefix(self):
        log = "abc1234 2026-03-09 Fix: uppercase prefix\n"
        result = wr.parse_git_log(log)
        assert result["by_type"]["fix"] == 1


# ---------------------------------------------------------------------------
# 6. fetch_git_log (subprocess mock)
# ---------------------------------------------------------------------------


class TestFetchGitLog:
    def test_returns_stdout_on_success(self):
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "abc1234 2026-03-09 feat: something\n"
        with patch("subprocess.run", return_value=mock_result):
            output = wr.fetch_git_log(
                workspace=Path("/fake"),
                since="2026-03-09",
                until="2026-03-15",
            )
        assert "feat: something" in output

    def test_returns_empty_string_on_git_failure(self):
        mock_result = MagicMock()
        mock_result.returncode = 128
        mock_result.stdout = ""
        with patch("subprocess.run", return_value=mock_result):
            output = wr.fetch_git_log(
                workspace=Path("/fake"),
                since="2026-03-09",
                until="2026-03-15",
            )
        assert output == ""

    def test_returns_empty_string_on_exception(self):
        with patch("subprocess.run", side_effect=FileNotFoundError("git not found")):
            output = wr.fetch_git_log(
                workspace=Path("/fake"),
                since="2026-03-09",
                until="2026-03-15",
            )
        assert output == ""


# ---------------------------------------------------------------------------
# 7. compute_fix_pct
# ---------------------------------------------------------------------------


class TestComputeFixPct:
    def test_fix_pct_basic(self):
        commits = {"total": 10, "by_type": {"fix": 3, "feat": 7}}
        assert wr.compute_fix_pct(commits) == pytest.approx(30.0)

    def test_fix_pct_zero_when_no_fix(self):
        commits = {"total": 10, "by_type": {"fix": 0, "feat": 10}}
        assert wr.compute_fix_pct(commits) == pytest.approx(0.0)

    def test_fix_pct_zero_when_no_commits(self):
        commits = {"total": 0, "by_type": {"fix": 0}}
        assert wr.compute_fix_pct(commits) == pytest.approx(0.0)

    def test_fix_pct_100(self):
        commits = {"total": 5, "by_type": {"fix": 5}}
        assert wr.compute_fix_pct(commits) == pytest.approx(100.0)


# ---------------------------------------------------------------------------
# 8. detect_anomalies
# ---------------------------------------------------------------------------


class TestDetectAnomalies:
    def test_fix_pct_anomaly_above_30(self):
        teams = {
            "dev2-team": {
                "task_count": 10,
                "fix_pct": 35.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 0, "medium": 10, "micro": 0},
            }
        }
        anomalies = wr.detect_anomalies(teams, prev_teams=None)
        assert any("fix_pct" in a and "dev2-team" in a for a in anomalies)
        assert any("35.0%" in a for a in anomalies)

    def test_no_fix_pct_anomaly_at_30(self):
        teams = {
            "dev1-team": {
                "task_count": 10,
                "fix_pct": 30.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 5, "medium": 5, "micro": 0},
            }
        }
        anomalies = wr.detect_anomalies(teams, prev_teams=None)
        assert not any("fix_pct" in a and "dev1-team" in a for a in anomalies)

    def test_productivity_drop_50pct(self):
        current = {
            "dev1-team": {
                "task_count": 4,
                "fix_pct": 10.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 2, "medium": 2, "micro": 0},
            }
        }
        prev = {
            "dev1-team": {
                "task_count": 10,
                "fix_pct": 10.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 5, "medium": 5, "micro": 0},
            }
        }
        anomalies = wr.detect_anomalies(current, prev_teams=prev)
        assert any("생산성" in a and "dev1-team" in a for a in anomalies)

    def test_no_productivity_anomaly_when_49pct_drop(self):
        current = {
            "dev1-team": {
                "task_count": 5,
                "fix_pct": 10.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 2, "medium": 3, "micro": 0},
            }
        }
        prev = {
            "dev1-team": {
                "task_count": 10,
                "fix_pct": 10.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 5, "medium": 5, "micro": 0},
            }
        }
        # 5/10 = 50% drop exactly -> threshold is > 50% drop, so this is boundary
        anomalies = wr.detect_anomalies(current, prev_teams=prev)
        # 50% drop => (10-5)/10 = 0.5 = 50% -> should NOT trigger (needs >50%)
        assert not any("생산성" in a and "dev1-team" in a for a in anomalies)

    def test_no_anomaly_when_no_prev_data(self):
        current = {
            "dev1-team": {
                "task_count": 5,
                "fix_pct": 10.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 2, "medium": 3, "micro": 0},
            }
        }
        anomalies = wr.detect_anomalies(current, prev_teams=None)
        # No prev data means no productivity drop anomaly
        assert not any("생산성" in a for a in anomalies)

    def test_multiple_anomalies_returned(self):
        teams = {
            "dev1-team": {
                "task_count": 2,
                "fix_pct": 40.0,
                "avg_duration_seconds": 500.0,
                "session_pattern": {"deep": 0, "medium": 2, "micro": 0},
            },
            "dev2-team": {
                "task_count": 2,
                "fix_pct": 50.0,
                "avg_duration_seconds": 500.0,
                "session_pattern": {"deep": 0, "medium": 2, "micro": 0},
            },
        }
        prev = {
            "dev1-team": {
                "task_count": 10,
                "fix_pct": 5.0,
                "avg_duration_seconds": 1000.0,
                "session_pattern": {"deep": 5, "medium": 5, "micro": 0},
            }
        }
        anomalies = wr.detect_anomalies(teams, prev_teams=prev)
        # fix_pct anomaly for dev1 and dev2, productivity drop for dev1
        assert len(anomalies) >= 3


# ---------------------------------------------------------------------------
# 9. compute_trend
# ---------------------------------------------------------------------------


class TestComputeTrend:
    def test_positive_task_count_change(self):
        curr = {
            "dev1-team": {
                "task_count": 12,
                "avg_duration_seconds": 1000.0,
                "fix_pct": 5.0,
                "session_pattern": {"deep": 5, "medium": 5, "micro": 2},
            }
        }
        prev = {
            "dev1-team": {
                "task_count": 10,
                "avg_duration_seconds": 1000.0,
                "fix_pct": 5.0,
                "session_pattern": {"deep": 5, "medium": 4, "micro": 1},
            }
        }
        trend = wr.compute_trend(curr, prev)
        assert trend["task_count_change_pct"] == pytest.approx(20.0)

    def test_negative_avg_duration_change(self):
        curr = {
            "dev1-team": {
                "task_count": 10,
                "avg_duration_seconds": 900.0,
                "fix_pct": 5.0,
                "session_pattern": {"deep": 3, "medium": 5, "micro": 2},
            }
        }
        prev = {
            "dev1-team": {
                "task_count": 10,
                "avg_duration_seconds": 1000.0,
                "fix_pct": 5.0,
                "session_pattern": {"deep": 3, "medium": 5, "micro": 2},
            }
        }
        trend = wr.compute_trend(curr, prev)
        assert trend["avg_duration_change_pct"] == pytest.approx(-10.0)

    def test_no_prev_data(self):
        curr = {
            "dev1-team": {
                "task_count": 10,
                "avg_duration_seconds": 900.0,
                "fix_pct": 5.0,
                "session_pattern": {"deep": 3, "medium": 5, "micro": 2},
            }
        }
        trend = wr.compute_trend(curr, None)
        assert trend == {}

    def test_zero_prev_task_count_no_division_error(self):
        curr = {
            "dev1-team": {
                "task_count": 5,
                "avg_duration_seconds": 1000.0,
                "fix_pct": 5.0,
                "session_pattern": {"deep": 2, "medium": 3, "micro": 0},
            }
        }
        prev = {
            "dev1-team": {
                "task_count": 0,
                "avg_duration_seconds": 0.0,
                "fix_pct": 5.0,
                "session_pattern": {"deep": 0, "medium": 0, "micro": 0},
            }
        }
        trend = wr.compute_trend(curr, prev)
        # Should not raise; result might be None or 0
        assert "task_count_change_pct" in trend


# ---------------------------------------------------------------------------
# 10. save_snapshot / load_snapshot
# ---------------------------------------------------------------------------


class TestSnapshotIO:
    def test_save_snapshot_creates_file(self, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        data = {"week": "2026-11", "teams": {}}
        wr.save_snapshot(data, snapshot_dir, "2026-11")
        expected = snapshot_dir / "week-2026-11.json"
        assert expected.exists()

    def test_save_snapshot_content(self, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        data = {"week": "2026-11", "teams": {"dev1-team": {"task_count": 5}}}
        wr.save_snapshot(data, snapshot_dir, "2026-11")
        saved = json.loads((snapshot_dir / "week-2026-11.json").read_text())
        assert saved["week"] == "2026-11"
        assert saved["teams"]["dev1-team"]["task_count"] == 5

    def test_save_snapshot_creates_dir_if_missing(self, tmp_path):
        snapshot_dir = tmp_path / "deep" / "nested" / "retro-snapshots"
        wr.save_snapshot({"week": "2026-11"}, snapshot_dir, "2026-11")
        assert snapshot_dir.exists()

    def test_load_snapshot_returns_data(self, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        snapshot_dir.mkdir()
        data = {"week": "2026-10", "teams": {"dev1-team": {"task_count": 8}}}
        (snapshot_dir / "week-2026-10.json").write_text(json.dumps(data))
        loaded = wr.load_snapshot(snapshot_dir, "2026-10")
        assert loaded is not None
        assert loaded["teams"]["dev1-team"]["task_count"] == 8

    def test_load_snapshot_returns_none_when_missing(self, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        snapshot_dir.mkdir()
        result = wr.load_snapshot(snapshot_dir, "2026-10")
        assert result is None


# ---------------------------------------------------------------------------
# 11. get_week_period
# ---------------------------------------------------------------------------


class TestGetWeekPeriod:
    def test_2026_week_11_period(self):
        start, end = wr.get_week_period("2026-11")
        assert str(start) == "2026-03-09"
        assert str(end) == "2026-03-15"

    def test_period_span_is_7_days(self):
        start, end = wr.get_week_period("2026-11")
        assert (end - start).days == 6

    def test_start_is_monday(self):
        start, _ = wr.get_week_period("2026-11")
        assert start.weekday() == 0  # Monday

    def test_end_is_sunday(self):
        _, end = wr.get_week_period("2026-11")
        assert end.weekday() == 6  # Sunday


# ---------------------------------------------------------------------------
# 12. get_current_week
# ---------------------------------------------------------------------------


class TestGetCurrentWeek:
    def test_returns_iso_week_format(self):
        week = wr.get_current_week()
        # Should match YYYY-WW pattern
        parts = week.split("-")
        assert len(parts) == 2
        year, wnum = int(parts[0]), int(parts[1])
        assert 2020 <= year <= 2100
        assert 1 <= wnum <= 53

    def test_format_is_zero_padded(self):
        with patch("weekly_retro.date") as mock_date:
            mock_date.today.return_value = date(2026, 1, 5)  # Week 2
            mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
            week = wr.get_current_week()
        assert week == "2026-02"


# ---------------------------------------------------------------------------
# 13. build_report (integration-level unit test)
# ---------------------------------------------------------------------------


class TestBuildReport:
    def test_report_has_required_keys(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = "abc1 2026-03-09 feat: f1\n" "abc2 2026-03-10 fix: f2\n" "abc3 2026-03-11 chore: c1\n"
        with patch("subprocess.run", return_value=mock_git):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )

        assert "week" in report
        assert "period" in report
        assert "teams" in report
        assert "commits" in report
        assert "trend" in report
        assert "anomalies" in report

    def test_report_week_matches_input(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = ""
        with patch("subprocess.run", return_value=mock_git):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        assert report["week"] == "2026-11"

    def test_report_period_dates(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = ""
        with patch("subprocess.run", return_value=mock_git):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        assert report["period"]["start"] == "2026-03-09"
        assert report["period"]["end"] == "2026-03-15"

    def test_report_teams_include_dev1(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = ""
        with patch("subprocess.run", return_value=mock_git):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        assert "dev1-team" in report["teams"]

    def test_report_with_empty_tasks(self, empty_tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = ""
        with patch("subprocess.run", return_value=mock_git):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=empty_tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        assert report["teams"] == {}
        assert report["anomalies"] == []

    def test_report_with_git_failure(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        with patch("subprocess.run", side_effect=FileNotFoundError):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        assert report["commits"]["total"] == 0

    def test_snapshot_saved_after_build(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = ""
        with patch("subprocess.run", return_value=mock_git):
            wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        assert (snapshot_dir / "week-2026-11.json").exists()

    def test_trend_uses_prev_snapshot(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        snapshot_dir.mkdir()
        # Write a previous week snapshot manually
        prev_data = {
            "week": "2026-10",
            "teams": {
                "dev1-team": {
                    "task_count": 20,
                    "avg_duration_seconds": 2000.0,
                    "fix_pct": 5.0,
                    "session_pattern": {"deep": 10, "medium": 8, "micro": 2},
                }
            },
        }
        (snapshot_dir / "week-2026-10.json").write_text(json.dumps(prev_data))
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = ""
        with patch("subprocess.run", return_value=mock_git):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        # Trend should be calculated
        assert report["trend"] != {}
        assert "task_count_change_pct" in report["trend"]

    def test_trend_no_prev_returns_empty_or_no_prev(self, tasks_json_file, tmp_path):
        snapshot_dir = tmp_path / "retro-snapshots"
        mock_git = MagicMock()
        mock_git.returncode = 0
        mock_git.stdout = ""
        with patch("subprocess.run", return_value=mock_git):
            report = wr.build_report(
                week="2026-11",
                workspace=tmp_path,
                tasks_file=tasks_json_file,
                snapshot_dir=snapshot_dir,
            )
        # No prev snapshot -> trend is empty dict or has no_previous_data marker
        assert isinstance(report["trend"], dict)


# ---------------------------------------------------------------------------
# 14. get_prev_week
# ---------------------------------------------------------------------------


class TestGetPrevWeek:
    def test_week_11_prev_is_10(self):
        assert wr.get_prev_week("2026-11") == "2026-10"

    def test_week_01_prev_is_last_year(self):
        prev = wr.get_prev_week("2026-01")
        # Should be last week of 2025
        assert prev.startswith("2025-")

    def test_week_52_prev_is_51(self):
        assert wr.get_prev_week("2025-52") == "2025-51"
