from datetime import datetime
import json
import os


def test_build_defined_ppt_jobs_uses_latest_date():
    from services.ppt_auto_generation_service import build_defined_ppt_jobs

    jobs = build_defined_ppt_jobs(latest_date="2026-05-11")
    by_type = {job.report_type: job for job in jobs}

    assert list(by_type) == [
        "daily", "weekly", "monthly", "quarterly", "half_yearly", "annual", "ttm",
        "strategy", "competitor", "competitor_v4", "promo", "promo_compare",
        "forecast_pre_event", "vendor", "category", "customer", "new_product",
        "market_intel", "price_elasticity",
    ]
    assert by_type["daily"].sub_arg == "2026/05/11"
    assert by_type["monthly"].sub_arg == "2026/05"
    assert by_type["quarterly"].sub_arg == "2026/Q2"
    assert by_type["half_yearly"].sub_arg == "2026/H1"
    assert by_type["annual"].sub_arg == "2026"
    assert by_type["strategy"].sub_arg == "2026/05/01-2026/05/11"
    assert by_type["market_intel"].sub_arg == "2026/05/11 起一週"
    assert by_type["competitor"].sub_arg == "monthly"
    assert by_type["promo"].sub_arg == "2026/05/05-2026/05/11"
    assert by_type["promo"].expected_params["label"] == "2026/05/05~2026/05/11"
    assert by_type["strategy"].expected_params == {
        "report_type": "strategy",
        "start": "2026/05/01",
        "end": "2026/05/11",
        "label": "2026/05 月策略（截至 05/11）",
    }


def test_auto_generation_respects_disabled_flag(monkeypatch):
    monkeypatch.setenv("PPT_AUTO_GENERATION_ENABLED", "false")

    from services.ppt_auto_generation_service import generate_defined_ppt_reports

    result = generate_defined_ppt_reports(report_types=["daily"])

    assert result["ok"] is False
    assert result["status"] == "disabled"


def test_dry_run_does_not_generate(monkeypatch):
    monkeypatch.setenv("PPT_AUTO_GENERATION_ENABLED", "true")

    from services import ppt_auto_generation_service as svc

    monkeypatch.setattr(svc, "_latest_sales_date", lambda: "2026-05-11")

    result = svc.generate_defined_ppt_reports(
        report_types=["daily", "monthly"],
        dry_run=True,
    )

    assert result["ok"] is True
    assert result["status"] == "planned"
    assert [job["report_type"] for job in result["jobs"]] == ["daily", "monthly"]


def test_coverage_marks_ready_from_database(monkeypatch):
    from services import ppt_auto_generation_service as svc

    class _Rows:
        def fetchall(self):
            return [
                (
                    "daily",
                    json.dumps({"report_type": "daily", "date": "2026/05/11"}),
                    "/tmp/ocbot_daily_ok.pptx",
                    datetime(2026, 5, 11, 20, 30),
                ),
                (
                    "monthly",
                    json.dumps({"report_type": "monthly", "month": "2026/05"}),
                    "/tmp/ocbot_monthly_ok.pptx",
                    datetime(2026, 5, 11, 20, 30),
                ),
            ]

    class _Session:
        def execute(self, *_args, **_kwargs):
            return _Rows()

        def close(self):
            return None

    monkeypatch.setattr(svc, "get_session", lambda: _Session())
    monkeypatch.setattr(svc, "_latest_sales_date", lambda: "2026-05-11")
    monkeypatch.setenv("PPT_AUTO_GENERATION_ENABLED", "true")

    result = svc.get_defined_report_coverage(
        month_start=datetime(2026, 5, 1),
        month_end=datetime(2026, 6, 1),
        reports_dir="/tmp/does-not-exist-for-test",
        report_types=["daily", "monthly", "weekly"],
    )

    by_key = {item["key"]: item for item in result["items"]}
    assert by_key["daily"]["ready"] is True
    assert by_key["daily"]["status"] == "ready"
    assert by_key["daily"]["status_label"] == "已產出"
    assert by_key["monthly"]["ready"] is True
    assert by_key["weekly"]["ready"] is False
    assert by_key["weekly"]["status"] == "missing"
    assert by_key["weekly"]["status_label"] == "待排程補齊"
    assert result["missing_count"] == 1


def test_due_schedule_kinds_include_periodic_boundaries():
    from services.ppt_auto_generation_service import get_due_schedule_kinds

    assert get_due_schedule_kinds(datetime(2026, 5, 18)) == ["daily", "weekly"]
    assert get_due_schedule_kinds(datetime(2026, 7, 1)) == [
        "daily",
        "monthly",
        "quarterly",
        "half_yearly",
    ]
    assert get_due_schedule_kinds(datetime(2026, 1, 1)) == [
        "daily",
        "monthly",
        "quarterly",
        "half_yearly",
        "annual",
    ]


def test_latest_schedule_occurrence_replays_missed_slots():
    from services.ppt_auto_generation_service import get_latest_schedule_occurrence

    assert get_latest_schedule_occurrence("daily", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 6, 5, 20, 30)
    assert get_latest_schedule_occurrence("daily", datetime(2026, 6, 6, 21, 0)) == datetime(2026, 6, 6, 20, 30)
    assert get_latest_schedule_occurrence("weekly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 6, 1, 20, 40)
    assert get_latest_schedule_occurrence("monthly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 6, 1, 20, 50)
    assert get_latest_schedule_occurrence("quarterly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 4, 1, 21, 0)
    assert get_latest_schedule_occurrence("half_yearly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 1, 1, 21, 10)
    assert get_latest_schedule_occurrence("annual", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 1, 1, 21, 20)


def test_ppt_catchup_plan_marks_missing_after_missed_slot(monkeypatch):
    from services import ppt_auto_generation_service as svc

    class _Rows:
        def fetchall(self):
            return []

    class _Session:
        def execute(self, *_args, **_kwargs):
            return _Rows()

        def close(self):
            return None

    monkeypatch.setattr(svc, "get_session", lambda: _Session())
    monkeypatch.setattr(svc, "_latest_sales_date", lambda: "2026-06-04")

    plan = svc.get_scheduled_ppt_catchup_plan(
        now=datetime(2026, 6, 6, 14, 0),
        schedule_kinds=["daily"],
    )

    assert plan[0]["scheduled_at"] == "2026-06-05 20:30"
    assert plan[0]["missing_report_types"] == ["daily"]
    assert plan[0]["ready"] is False


def test_ppt_catchup_plan_uses_existing_exact_report(monkeypatch, tmp_path):
    from services import ppt_auto_generation_service as svc

    pptx = tmp_path / "ocbot_daily_ok.pptx"
    pptx.write_bytes(b"pptx")

    class _Rows:
        def fetchall(self):
            return [
                (
                    "daily",
                    json.dumps({"report_type": "daily", "date": "2026/06/04"}),
                    str(pptx),
                )
            ]

    class _Session:
        def execute(self, *_args, **_kwargs):
            return _Rows()

        def close(self):
            return None

    monkeypatch.setattr(svc, "get_session", lambda: _Session())
    monkeypatch.setattr(svc, "_latest_sales_date", lambda: "2026-06-04")

    plan = svc.get_scheduled_ppt_catchup_plan(
        now=datetime(2026, 6, 6, 14, 0),
        schedule_kinds=["daily"],
    )

    assert plan[0]["ready_report_types"] == ["daily"]
    assert plan[0]["missing_report_types"] == []
    assert plan[0]["ready"] is True


def test_ppt_catchup_generates_only_missing_types(monkeypatch):
    from services import ppt_auto_generation_service as svc

    calls = []

    monkeypatch.setattr(
        svc,
        "get_scheduled_ppt_catchup_plan",
        lambda **_kwargs: [{
            "schedule_kind": "weekly",
            "scheduled_at": "2026-06-01 20:40",
            "missing_report_types": ["market_intel"],
        }],
    )

    def fake_generate_defined_ppt_reports(**kwargs):
        calls.append(kwargs)
        return {"ok": True, "ready": 1, "errors": 0, "jobs": [{"report_type": "market_intel"}]}

    monkeypatch.setattr(svc, "generate_defined_ppt_reports", fake_generate_defined_ppt_reports)

    result = svc.catch_up_scheduled_ppt_reports()

    assert result["status"] == "completed"
    assert result["generated_kinds"] == ["weekly"]
    assert calls == [{
        "report_types": ["market_intel"],
        "schedule_kind": "weekly",
        "force": False,
    }]


def test_ppt_catchup_background_queues_without_main_loop_block(monkeypatch):
    from services import ppt_auto_generation_service as svc

    calls = []
    threads = []

    class _Thread:
        def __init__(self, *, target, name, daemon):
            self.target = target
            self.name = name
            self.daemon = daemon
            threads.append(self)

        def start(self):
            self.target()

    def fake_catchup(**kwargs):
        calls.append(kwargs)
        return {"ok": True, "status": "skipped", "runs": []}

    monkeypatch.setattr(svc.threading, "Thread", _Thread)
    monkeypatch.setattr(svc, "catch_up_scheduled_ppt_reports", fake_catchup)

    result = svc.start_scheduled_ppt_catchup_background(schedule_kinds=["daily", "weekly"])

    assert result["status"] == "queued"
    assert result["schedule_kinds"] == ["daily", "weekly"]
    assert threads[0].name == "ppt-auto-generation-catchup"
    assert threads[0].daemon is True
    assert calls == [{
        "now": None,
        "force": False,
        "schedule_kinds": ["daily", "weekly"],
    }]


def test_scheduled_generation_sets_fast_fallback_env(monkeypatch, tmp_path):
    from routes import openclaw_bot_routes as bot_routes
    from services import ppt_auto_generation_service as svc

    output = tmp_path / "ocbot_market_intel_ok.pptx"
    output.write_bytes(b"pptx")
    observed = {}
    job = svc.build_defined_ppt_jobs(
        latest_date="2026-05-11",
        report_types=["market_intel"],
    )[0]

    monkeypatch.delenv("PPT_SCHEDULED_FAST_FALLBACK", raising=False)
    monkeypatch.delenv("MCP_FAST_STATIC_FALLBACK", raising=False)
    monkeypatch.setattr(svc, "_expire_matching_ppt_cache", lambda _job: 0)
    monkeypatch.setattr(bot_routes, "send_message", lambda *_args, **_kwargs: None, raising=False)

    def fake_generate_ppt_cmd(*_args, **_kwargs):
        observed["ppt_fast"] = os.getenv("PPT_SCHEDULED_FAST_FALLBACK")
        observed["mcp_fast"] = os.getenv("MCP_FAST_STATIC_FALLBACK")
        return str(output)

    monkeypatch.setattr(bot_routes, "_generate_ppt_cmd", fake_generate_ppt_cmd)

    path, invalidated = svc._generate_job(job, schedule_kind="weekly")

    assert path == str(output)
    assert invalidated == 0
    assert observed == {"ppt_fast": "true", "mcp_fast": "true"}
    assert os.getenv("PPT_SCHEDULED_FAST_FALLBACK") is None
    assert os.getenv("MCP_FAST_STATIC_FALLBACK") is None


def test_schedule_cadence_status_exposes_all_periodic_contracts():
    from services.ppt_auto_generation_service import get_schedule_cadence_status

    cadences = get_schedule_cadence_status([
        {"key": "daily", "ready": True},
        {"key": "weekly", "ready": False},
        {"key": "market_intel", "ready": True},
    ])
    by_key = {cadence["key"]: cadence for cadence in cadences}

    assert [cadence["schedule_text"] for cadence in cadences] == [
        "每日 20:30",
        "每週一 20:40",
        "每月 1 日 20:50",
        "每季首日 21:00",
        "每半年首日 21:10",
        "每年 1/1 21:20",
    ]
    assert by_key["weekly"]["report_types"] == ["weekly", "market_intel"]
    assert by_key["weekly"]["ready_count"] == 1
    assert by_key["weekly"]["missing_report_types"] == ["weekly"]
    assert by_key["weekly"]["missing_report_labels"] == ["週報"]
    assert by_key["weekly"]["status_label"] == "已完成 1/2"
    assert by_key["weekly"]["coverage_text"] == "1/2"
    assert "TTM 滾動 12 月" in by_key["monthly"]["report_labels"]


def test_scheduled_generation_uses_profile_without_generating(monkeypatch):
    from services import ppt_auto_generation_service as svc

    calls = []

    def fake_generate_defined_ppt_reports(**kwargs):
        calls.append(kwargs)
        return {"ok": True, "ready": 2, "errors": 0, "jobs": [{"report_type": "weekly"}]}

    monkeypatch.setattr(svc, "generate_defined_ppt_reports", fake_generate_defined_ppt_reports)

    result = svc.generate_scheduled_ppt_reports(schedule_kind="weekly", force=True)

    assert result["ok"] is True
    assert result["schedule_kinds"] == ["weekly"]
    assert calls == [{
        "report_types": ("weekly", "market_intel"),
        "schedule_kind": "weekly",
        "force": True,
    }]


def test_force_regeneration_expires_only_matching_cache(monkeypatch):
    from services import ppt_auto_generation_service as svc

    captured = {}

    class _Result:
        rowcount = 1

    class _Session:
        def execute(self, sql, params):
            captured["sql"] = str(sql)
            captured["params"] = dict(params)
            return _Result()

        def commit(self):
            captured["committed"] = True

        def close(self):
            captured["closed"] = True

    monkeypatch.setattr(svc, "get_session", lambda: _Session())
    monkeypatch.setattr(svc, "_normalize_ppt_cache_parameters", lambda params: "normalized-cache-key")

    job = svc.build_defined_ppt_jobs(latest_date="2026-05-11", report_types=["daily"])[0]
    affected = svc._expire_matching_ppt_cache(job)

    assert affected == 1
    assert "UPDATE ppt_reports" in captured["sql"]
    assert "report_type = :report_type" in captured["sql"]
    assert "parameters = :parameters" in captured["sql"]
    assert captured["params"]["report_type"] == "daily"
    assert captured["params"]["parameters"] == "normalized-cache-key"
    assert captured["committed"] is True
    assert captured["closed"] is True


def test_ppt_observability_single_regeneration_forces_cache_refresh():
    from pathlib import Path

    js = Path("web/static/js/observability-charts.js").read_text(encoding="utf-8")

    assert "force: Boolean(force)" in js
    assert "triggerGeneration(false, [reportType], singleButton, label, true)" in js


def test_ppt_preview_reports_missing_converter(monkeypatch, tmp_path):
    from services import ppt_preview_service as svc

    pptx = tmp_path / "demo.pptx"
    pptx.write_bytes(b"fake pptx")
    monkeypatch.setattr(svc.shutil, "which", lambda _name: None)

    result = svc.build_ppt_preview(pptx, cache_dir=tmp_path / "cache")

    assert result.ok is False
    assert "LibreOffice" in (result.error or "")


def test_ppt_preview_cache_info_is_read_only(tmp_path):
    from pathlib import Path
    from services import ppt_preview_service as svc

    pptx = tmp_path / "demo.pptx"
    pptx.write_bytes(b"fake pptx")
    cache_dir = tmp_path / "cache"

    miss = svc.get_ppt_preview_cache_info(pptx, cache_dir=cache_dir)
    assert miss.pdf_path
    assert miss.cache_exists is False

    Path(miss.pdf_path).parent.mkdir(parents=True, exist_ok=True)
    Path(miss.pdf_path).write_bytes(b"%PDF-1.4\n")

    hit = svc.get_ppt_preview_cache_info(pptx, cache_dir=cache_dir)
    assert hit.cache_exists is True
    assert hit.cache_size_kb is not None
