"""Tests for the state expiry scheduler."""
import os
import time
import pytest
from unittest.mock import patch, MagicMock
from flask import Flask
from tinydb import Query

from utils.state_expiry_scheduler import (
    _get_today_start_timestamp,
    expire_stale_pending_for_user,
    run_state_expiry_check,
)
from db.db import users_db, pending_confirmations_db
from tests.conftest import TEST_SECRET_KEY

EXPIRY_USER_ID = "expiry_test_user"
EXPIRY_USER_ID_2 = "expiry_test_user_2"
EXPIRY_CHILD_ID = "expiry_child_id"
EXPIRY_CHILD_ID_2 = "expiry_child_id_2"
EXPIRY_TASK_ID = "expiry_task_id"
EXPIRY_REWARD_ID = "expiry_reward_id"


def _yesterday_timestamp() -> float:
    """Return a Unix timestamp from 25 hours ago (safely before today's midnight)."""
    return time.time() - (25 * 3600)


def _seed_user(user_id: str = EXPIRY_USER_ID, timezone: str = "UTC", verified: bool = True):
    users_db.remove(Query().id == user_id)
    users_db.insert({
        "id": user_id,
        "first_name": "Expiry",
        "last_name": "Tester",
        "email": f"{user_id}@example.com",
        "password": "hashed",
        "verified": verified,
        "role": "user",
        "image_id": None,
        "marked_for_deletion": False,
        "timezone": timezone,
        "email_digest_enabled": False,
    })


def _seed_pending(
    pending_id: str,
    user_id: str = EXPIRY_USER_ID,
    child_id: str = EXPIRY_CHILD_ID,
    entity_id: str = EXPIRY_TASK_ID,
    entity_type: str = "chore",
    status: str = "pending",
    created_at: float = None,
):
    pending_confirmations_db.remove(Query().id == pending_id)
    pending_confirmations_db.insert({
        "id": pending_id,
        "user_id": user_id,
        "child_id": child_id,
        "entity_id": entity_id,
        "entity_type": entity_type,
        "status": status,
        "approved_at": None,
        "created_at": created_at if created_at is not None else _yesterday_timestamp(),
        "updated_at": time.time(),
    })


def _cleanup():
    for uid in (EXPIRY_USER_ID, EXPIRY_USER_ID_2):
        users_db.remove(Query().id == uid)
        pending_confirmations_db.remove(Query().user_id == uid)


@pytest.fixture
def app():
    flask_app = Flask(__name__)
    flask_app.config['TESTING'] = True
    flask_app.config['SECRET_KEY'] = TEST_SECRET_KEY
    return flask_app


class TestGetTodayStartTimestamp:
    def test_returns_float(self):
        ts = _get_today_start_timestamp("UTC")
        assert isinstance(ts, float)

    def test_today_start_is_before_now(self):
        ts = _get_today_start_timestamp("UTC")
        assert ts <= time.time()

    def test_falls_back_to_utc_on_none(self):
        ts_utc = _get_today_start_timestamp("UTC")
        ts_none = _get_today_start_timestamp(None)
        assert abs(ts_utc - ts_none) < 1  # within 1 second

    def test_falls_back_to_utc_on_invalid_timezone(self):
        ts_utc = _get_today_start_timestamp("UTC")
        ts_bad = _get_today_start_timestamp("Invalid/Timezone")
        assert abs(ts_utc - ts_bad) < 1

    def test_timezone_shifts_midnight(self):
        """UTC-12 midnight is 12 hours later than UTC midnight in absolute terms."""
        ts_utc = _get_today_start_timestamp("UTC")
        ts_west = _get_today_start_timestamp("Etc/GMT+12")
        # The western timezone's "today start" is at most 12 hours away from UTC's
        assert abs(ts_utc - ts_west) <= 12 * 3600 + 1


class TestExpireStaleForUser:
    def setup_method(self):
        _cleanup()

    def teardown_method(self):
        _cleanup()

    def test_pending_chore_from_yesterday_is_deleted(self, app):
        _seed_pending("exp_chore_1", entity_type="chore", created_at=_yesterday_timestamp())
        with app.app_context():
            with patch('events.sse.send_event_to_user') as mock_send:
                count = expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        assert count == 1
        assert pending_confirmations_db.get(Query().id == "exp_chore_1") is None

    def test_pending_chore_fires_reset_sse(self, app):
        _seed_pending("exp_chore_2", entity_type="chore", created_at=_yesterday_timestamp())
        with app.app_context():
            with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
                expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        mock_send.assert_called_once()
        event = mock_send.call_args[0][1]
        assert event.type == "child_chore_confirmation"
        assert event.payload.data['operation'] == "RESET"
        assert event.payload.data['task_id'] == EXPIRY_TASK_ID

    def test_pending_reward_from_yesterday_is_deleted(self, app):
        _seed_pending(
            "exp_reward_1",
            entity_id=EXPIRY_REWARD_ID,
            entity_type="reward",
            created_at=_yesterday_timestamp(),
        )
        with app.app_context():
            with patch('utils.state_expiry_scheduler.send_event_to_user'):
                count = expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        assert count == 1
        assert pending_confirmations_db.get(Query().id == "exp_reward_1") is None

    def test_pending_reward_fires_cancelled_sse(self, app):
        _seed_pending(
            "exp_reward_2",
            entity_id=EXPIRY_REWARD_ID,
            entity_type="reward",
            created_at=_yesterday_timestamp(),
        )
        with app.app_context():
            with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
                expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        mock_send.assert_called_once()
        event = mock_send.call_args[0][1]
        assert event.type == "child_reward_request"
        assert event.payload.data['operation'] == "CANCELLED"
        assert event.payload.data['reward_id'] == EXPIRY_REWARD_ID

    def test_pending_record_from_today_is_not_deleted(self, app):
        _seed_pending("exp_today_1", created_at=time.time())
        with app.app_context():
            with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
                count = expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        assert count == 0
        assert pending_confirmations_db.get(Query().id == "exp_today_1") is not None
        mock_send.assert_not_called()

    def test_approved_record_from_yesterday_is_not_deleted(self, app):
        _seed_pending("exp_approved_1", status="approved", created_at=_yesterday_timestamp())
        with app.app_context():
            with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
                count = expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        assert count == 0
        assert pending_confirmations_db.get(Query().id == "exp_approved_1") is not None
        mock_send.assert_not_called()

    def test_no_pending_records_returns_zero(self, app):
        with app.app_context():
            with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
                count = expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        assert count == 0
        mock_send.assert_not_called()

    def test_multiple_stale_records_all_expired(self, app):
        _seed_pending("exp_multi_1", entity_id=EXPIRY_TASK_ID, entity_type="chore",
                      created_at=_yesterday_timestamp())
        _seed_pending("exp_multi_2", entity_id=EXPIRY_REWARD_ID, entity_type="reward",
                      created_at=_yesterday_timestamp())
        with app.app_context():
            with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
                count = expire_stale_pending_for_user(EXPIRY_USER_ID, "UTC")
        assert count == 2
        assert mock_send.call_count == 2


class TestRunStateExpiryCheck:
    def setup_method(self):
        _cleanup()

    def teardown_method(self):
        _cleanup()

    def test_expires_stale_records_for_all_verified_users(self, app):
        _seed_user(EXPIRY_USER_ID)
        _seed_user(EXPIRY_USER_ID_2)
        _seed_pending("exp_u1", user_id=EXPIRY_USER_ID, created_at=_yesterday_timestamp())
        _seed_pending("exp_u2", user_id=EXPIRY_USER_ID_2, child_id=EXPIRY_CHILD_ID_2,
                      created_at=_yesterday_timestamp())
        with patch('utils.state_expiry_scheduler.send_event_to_user'):
            run_state_expiry_check(app)
        assert pending_confirmations_db.get(Query().id == "exp_u1") is None
        assert pending_confirmations_db.get(Query().id == "exp_u2") is None

    def test_only_expires_stale_records_not_todays(self, app):
        _seed_user(EXPIRY_USER_ID)
        _seed_pending("exp_stale", created_at=_yesterday_timestamp())
        _seed_pending("exp_fresh", created_at=time.time())
        with patch('utils.state_expiry_scheduler.send_event_to_user'):
            run_state_expiry_check(app)
        assert pending_confirmations_db.get(Query().id == "exp_stale") is None
        assert pending_confirmations_db.get(Query().id == "exp_fresh") is not None

    def test_skips_when_db_env_is_e2e(self, app):
        _seed_user(EXPIRY_USER_ID)
        _seed_pending("exp_e2e", created_at=_yesterday_timestamp())
        original = os.environ.get('DB_ENV')
        try:
            os.environ['DB_ENV'] = 'e2e'
            with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
                run_state_expiry_check(app)
            mock_send.assert_not_called()
            assert pending_confirmations_db.get(Query().id == "exp_e2e") is not None
        finally:
            if original is not None:
                os.environ['DB_ENV'] = original
            else:
                os.environ.pop('DB_ENV', None)

    def test_no_error_when_user_has_no_pending_records(self, app):
        _seed_user(EXPIRY_USER_ID)
        with patch('utils.state_expiry_scheduler.send_event_to_user') as mock_send:
            run_state_expiry_check(app)
        mock_send.assert_not_called()
