"""Original Software Made Clear teaching examples; not a booking application.

Python 3.11+ with the standard sqlite3 module. No packages, network or disk DB.
Default: all checks pass. --demo selects an intentionally weaker/broken variant.
Run `python3 booking_tests.py --help` for the explicitly labelled demonstrations.
"""
from __future__ import annotations

import argparse
import sqlite3
import unittest
from dataclasses import dataclass


# snippet:deadline
DEADLINE = 18 * 60  # Minutes after midnight; one already-agreed local day.


def can_cancel(minute: int) -> bool:
    return minute < DEADLINE


def can_cancel_buggy(minute: int) -> bool:
    return minute <= DEADLINE  # Deliberate mistake: accepts exactly 18:00.
# end:deadline


# snippet:cancellation
@dataclass
class Booking:
    cancelled: bool = False


def cancel_original(booking: Booking) -> str:
    if booking.cancelled:
        return "already_cancelled"
    booking.cancelled = True
    return "cancelled"


def mark_cancelled(booking: Booking) -> None:
    booking.cancelled = True


def cancel_refactored(booking: Booking) -> str:
    if booking.cancelled:
        return "already_cancelled"
    mark_cancelled(booking)
    return "cancelled"


def cancel_broken(booking: Booking) -> str:
    if booking.cancelled:
        raise ValueError("already cancelled")  # Unrequested behaviour change.
    mark_cancelled(booking)
    return "cancelled"
# end:cancellation


# snippet:storage
class FakeBookings:
    """Deliberately incomplete substitute: does not enforce uniqueness."""

    def __init__(self) -> None:
        self.rows: list[tuple[int, str]] = []

    def add(self, booking_id: int, slot: str) -> None:
        self.rows.append((booking_id, slot))

    def count(self) -> int:
        return len(self.rows)

    def close(self) -> None:
        pass


class SQLiteBookings:
    """Tiny adapter: this example models one reservation per slot, no expiry."""

    def __init__(self) -> None:
        self.db = sqlite3.connect(":memory:")
        self.db.execute(
            "CREATE TABLE bookings ("
            "id INTEGER PRIMARY KEY, slot TEXT NOT NULL UNIQUE)"
        )

    def add(self, booking_id: int, slot: str) -> None:
        with self.db:
            self.db.execute(
                "INSERT INTO bookings (id, slot) VALUES (?, ?)",
                (booking_id, slot),
            )

    def count(self) -> int:
        return self.db.execute("SELECT count(*) FROM bookings").fetchone()[0]

    def close(self) -> None:
        self.db.close()
# end:storage


# snippet:weak-tests
class WeakDeadlineTests(unittest.TestCase):
    rule = staticmethod(can_cancel)

    def test_before_deadline_is_allowed(self) -> None:
        self.assertTrue(self.rule(17 * 60))

    def test_after_deadline_is_rejected(self) -> None:
        self.assertFalse(self.rule(19 * 60))
# end:weak-tests


# snippet:boundary-tests
class BoundaryDeadlineTests(WeakDeadlineTests):
    # Inherits the 17:00 and 19:00 checks and adds the missing boundary.
    def test_exact_deadline_is_rejected(self) -> None:
        self.assertFalse(self.rule(18 * 60))

    def test_last_minute_before_deadline_is_allowed(self) -> None:
        self.assertTrue(self.rule(17 * 60 + 59))

    def test_first_minute_after_deadline_is_rejected(self) -> None:
        self.assertFalse(self.rule(18 * 60 + 1))
# end:boundary-tests


# snippet:characterization-tests
class CancellationTests(unittest.TestCase):
    cancel = staticmethod(cancel_refactored)

    def test_first_request_cancels_booking(self) -> None:
        booking = Booking()
        self.assertEqual(self.cancel(booking), "cancelled")
        self.assertTrue(booking.cancelled)

    def test_second_request_returns_existing_result(self) -> None:
        booking = Booking()
        self.cancel(booking)
        self.assertEqual(self.cancel(booking), "already_cancelled")
        self.assertTrue(booking.cancelled)

    def test_previously_cancelled_booking_keeps_state(self) -> None:
        booking = Booking(cancelled=True)
        self.assertEqual(self.cancel(booking), "already_cancelled")
        self.assertTrue(booking.cancelled)
# end:characterization-tests


# snippet:database-tests
class DatabaseTests(unittest.TestCase):
    repository_type = SQLiteBookings

    def setUp(self) -> None:
        self.repository = self.repository_type()
        self.addCleanup(self.repository.close)

    def test_one_booking_is_stored(self) -> None:
        self.repository.add(1, "room-a-1800")
        self.assertEqual(self.repository.count(), 1)

    def test_duplicate_slot_is_rejected(self) -> None:
        self.repository.add(1, "room-a-1800")
        with self.assertRaises(sqlite3.IntegrityError):
            self.repository.add(2, "room-a-1800")
        self.assertEqual(self.repository.count(), 1)

    def test_different_slots_are_allowed(self) -> None:
        self.repository.add(1, "room-a-1800")
        self.repository.add(2, "room-b-1800")
        self.assertEqual(self.repository.count(), 2)
# end:database-tests


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--demo",
        choices=("weak", "boundary", "legacy", "refactor", "fake", "database"),
        help="weak/legacy/database pass; boundary/refactor/fake intentionally fail",
    )
    args = parser.parse_args()
    # Reset selections so even repeated in-process invocations are independent.
    WeakDeadlineTests.rule = staticmethod(can_cancel)
    CancellationTests.cancel = staticmethod(cancel_refactored)
    DatabaseTests.repository_type = SQLiteBookings
    cases = [BoundaryDeadlineTests, CancellationTests, DatabaseTests]
    if args.demo in ("weak", "boundary"):
        WeakDeadlineTests.rule = staticmethod(can_cancel_buggy)
        cases = [WeakDeadlineTests if args.demo == "weak" else BoundaryDeadlineTests]
    elif args.demo in ("legacy", "refactor"):
        CancellationTests.cancel = staticmethod(
            cancel_original if args.demo == "legacy" else cancel_broken
        )
        cases = [CancellationTests]
    elif args.demo in ("fake", "database"):
        if args.demo == "fake":
            DatabaseTests.repository_type = FakeBookings
        cases = [DatabaseTests]
    suite = unittest.TestSuite(
        unittest.defaultTestLoader.loadTestsFromTestCase(case) for case in cases
    )
    result = unittest.TextTestRunner(verbosity=2).run(suite)
    return 0 if result.wasSuccessful() else 1


if __name__ == "__main__":
    raise SystemExit(main())
