import datetime
from contextlib import redirect_stdout
import importlib.util
import io
import json
from pathlib import Path
from types import SimpleNamespace
from unittest import TestCase


SCRIPT = Path(__file__).resolve().parents[2] / "deploy" / "phase1_term_e2e_attest.py"
SPEC = importlib.util.spec_from_file_location("phase1_term_e2e_attest", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(MODULE)


class Phase1TermE2EAttestationTests(TestCase):
    def test_selects_one_exact_january_to_june_term_with_46_activities(self):
        selected = MODULE.select_term_candidate(
            [
                {
                    "id": 65,
                    "start_date": datetime.date(2026, 1, 5),
                    "end_date": datetime.date(2026, 6, 28),
                    "activity_count": 46,
                    "status": 1,
                },
                {
                    "id": 66,
                    "start_date": datetime.date(2026, 2, 1),
                    "end_date": datetime.date(2026, 6, 30),
                    "activity_count": 46,
                    "status": 1,
                },
            ]
        )

        self.assertEqual(selected["id"], 65)

    def test_resolves_term_independently_of_claimed_activity_count(self):
        candidate = {
            "id": 65,
            "start_date": datetime.date(2026, 1, 1),
            "end_date": datetime.date(2026, 6, 30),
            "activity_count": 45,
            "status": 1,
        }
        self.assertEqual(MODULE.select_term_candidate([candidate])["id"], 65)

        candidate["activity_count"] = 46
        with self.assertRaisesRegex(MODULE.TermAttestationError, "exactly one") as caught:
            MODULE.select_term_candidate([candidate, {**candidate, "id": 66}])
        self.assertEqual(caught.exception.details["strict_match_count"], 2)
        self.assertEqual(
            caught.exception.details["overlapping_term_candidates"][0]["start_date"],
            "2026-01-01",
        )

    def test_workflow_is_read_only_and_never_receives_api_credentials(self):
        workflow = (
            Path(__file__).resolve().parents[2]
            / ".github"
            / "workflows"
            / "deploy.yml"
        ).read_text()
        self.assertIn("SET TRANSACTION READ ONLY", SCRIPT.read_text())
        term_job = workflow.split("  phase1-term-attestation:", 1)[1]
        self.assertIn("TT_LOAD_CONFIRM_MUTATION", term_job)
        self.assertIn("TT_LOAD_CONFIRM_OPERATIONAL", term_job)
        self.assertNotIn("TT_PHASE1_LOAD_PASSWORD", term_job)
        self.assertNotIn("TT_PHASE1_LOAD_EMAIL", term_job)
        self.assertNotIn("schedule-request", term_job)
        self.assertNotIn("preschedule-request", term_job)
        self.assertNotIn("ResourceBooking", SCRIPT.read_text())
        self.assertIn('"term_activity_inventory"', SCRIPT.read_text())
        self.assertIn('"term_attestation_complete"', SCRIPT.read_text())

    def test_backfill_workflow_separates_plan_execute_and_verify(self):
        workflow = (
            Path(__file__).resolve().parents[2]
            / ".github"
            / "workflows"
            / "deploy.yml"
        ).read_text()
        section = workflow.split("  phase1-term-projection-backfill:", 1)[1]
        self.assertIn("plan-jan-jun-2026-backfill", section)
        self.assertIn("execute-jan-jun-2026-backfill", section)
        self.assertIn("verify-jan-jun-2026-backfill", section)
        self.assertIn("TT_PHASE1_TERM_BACKFILL_PLAN_SHA256", section)
        self.assertIn("TT_PHASE1_TERM_CONFIRM_OUTBOX_MUTATION", section)
        self.assertIn('test "$TT_PHASE1_TERM_EXPECTED_WATERMARK" = "2639"', section)
        self.assertIn("deploy/phase1_term_projection_backfill.py", section)
        self.assertNotIn("TT_PHASE1_LOAD_EMAIL", section)
        self.assertNotIn("TT_PHASE1_LOAD_PASSWORD", section)
        self.assertNotIn("RB_PHASE1_E2E", section)

    def test_projection_summary_attributes_tombstone_without_domain_row(self):
        current = {
            "id": 101,
            "academic_term_id": 26,
        }
        rows = [
            SimpleNamespace(
                id=1,
                aggregate_id="default:101",
                event_type="timetabler.activity.snapshot",
                event_version=1,
                transport_sequence=40,
                payload={
                    "committed_state": {
                        "replacement": {"previous": None, "current": current}
                    }
                },
            ),
            SimpleNamespace(
                id=2,
                aggregate_id="default:101",
                event_type="timetabler.activity.deleted",
                event_version=2,
                transport_sequence=41,
                payload={
                    "committed_state": {
                        "replacement": {"previous": current, "current": None}
                    }
                },
            ),
        ]

        at_40 = MODULE.summarize_activity_projection(
            rows, watermark=40, academic_term_id=26
        )
        at_41 = MODULE.summarize_activity_projection(
            rows, watermark=41, academic_term_id=26
        )

        self.assertEqual(at_40["term_live_activity_ids"], [101])
        self.assertEqual(at_41["term_live_activity_count"], 0)
        self.assertEqual(at_41["term_latest_tombstone_ids"], [101])

    def test_bounded_fact_output_chunks_large_lists(self):
        output = io.StringIO()
        with redirect_stdout(output):
            MODULE._emit_bounded_fact(
                ["ids"], list(range(500)), max_bytes=300
            )
        records = [json.loads(line) for line in output.getvalue().splitlines()]
        rebuilt = [
            item
            for record in records
            for item in record["term_attestation_fact"]["value"]
        ]
        self.assertEqual(rebuilt, list(range(500)))
        self.assertTrue(all(len(line.encode()) <= 300 for line in output.getvalue().splitlines()))
