import pytest
from tests.conftest import TEST_SECRET_KEY, TEST_REFRESH_TOKEN_EXPIRY_DAYS
import os
from werkzeug.security import generate_password_hash

from flask import Flask
from api.chore_schedule_api import chore_schedule_api
from api.auth_api import auth_api
from db.db import users_db, child_db, chore_schedules_db, task_extensions_db, pending_confirmations_db, task_db
from tinydb import Query
from models.pending_confirmation import PendingConfirmation


TEST_EMAIL = "sched_test@example.com"
TEST_PASSWORD = "testpass"
TEST_CHILD_ID = "sched-child-1"
TEST_TASK_ID = "sched-task-1"
TEST_USER_ID = "sched-user-1"


def add_test_user():
    users_db.remove(Query().email == TEST_EMAIL)
    users_db.insert({
        "id": TEST_USER_ID,
        "first_name": "Sched",
        "last_name": "Tester",
        "email": TEST_EMAIL,
        "password": generate_password_hash(TEST_PASSWORD),
        "verified": True,
        "image_id": "",
    })


def add_test_child():
    child_db.remove(Query().id == TEST_CHILD_ID)
    child_db.insert({
        "id": TEST_CHILD_ID,
        "user_id": TEST_USER_ID,
        "name": "Test Child",
        "points": 0,
        "image_id": "",
        "tasks": [TEST_TASK_ID],
        "rewards": [],
    })


def login_and_set_cookie(client):
    resp = client.post('/auth/login', json={"email": TEST_EMAIL, "password": TEST_PASSWORD})
    assert resp.status_code == 200


@pytest.fixture
def client():
    app = Flask(__name__)
    app.register_blueprint(chore_schedule_api)
    app.register_blueprint(auth_api, url_prefix='/auth')
    app.config['TESTING'] = True
    app.config['SECRET_KEY'] = TEST_SECRET_KEY
    app.config['REFRESH_TOKEN_EXPIRY_DAYS'] = TEST_REFRESH_TOKEN_EXPIRY_DAYS
    with app.test_client() as client:
        add_test_user()
        add_test_child()
        chore_schedules_db.truncate()
        task_extensions_db.truncate()
        login_and_set_cookie(client)
        yield client


# ---------------------------------------------------------------------------
# GET schedule
# ---------------------------------------------------------------------------

def test_get_schedule_not_found(client):
    resp = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp.status_code == 404


def test_get_schedule_returns_404_for_unknown_child(client):
    resp = client.get('/child/bad-child/task/bad-task/schedule')
    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# PUT (set) schedule – days mode
# ---------------------------------------------------------------------------

def test_set_schedule_days_mode(client):
    payload = {
        "mode": "days",
        "day_configs": [
            {"day": 1, "hour": 8, "minute": 0},
            {"day": 3, "hour": 9, "minute": 30},
        ],
    }
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200
    data = resp.get_json()
    assert data["mode"] == "days"
    assert len(data["day_configs"]) == 2
    assert data["child_id"] == TEST_CHILD_ID
    assert data["task_id"] == TEST_TASK_ID


def test_get_schedule_after_set(client):
    payload = {"mode": "days", "day_configs": [{"day": 0, "hour": 7, "minute": 0}]}
    client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)

    resp = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp.status_code == 200
    data = resp.get_json()
    assert data["mode"] == "days"
    assert data["day_configs"][0]["day"] == 0


# ---------------------------------------------------------------------------
# PUT (set) schedule – interval mode
# ---------------------------------------------------------------------------

def test_set_schedule_interval_mode(client):
    payload = {
        "mode": "interval",
        "interval_days": 3,
        "anchor_date": "2026-03-01",
        "interval_has_deadline": True,
        "interval_hour": 14,
        "interval_minute": 30,
    }
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200
    data = resp.get_json()
    assert data["mode"] == "interval"
    assert data["interval_days"] == 3
    assert data["anchor_date"] == "2026-03-01"
    assert data["interval_has_deadline"] is True
    assert data["interval_hour"] == 14
    assert data["interval_minute"] == 30


def test_set_schedule_interval_days_1_valid(client):
    """interval_days=1 is now valid (range changed to [1, 7])."""
    payload = {"mode": "interval", "interval_days": 1, "anchor_date": ""}
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200
    assert resp.get_json()["interval_days"] == 1


def test_set_schedule_interval_days_0_invalid(client):
    """interval_days=0 is still out of range."""
    payload = {"mode": "interval", "interval_days": 0, "anchor_date": ""}
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 400


def test_set_schedule_interval_days_8_invalid(client):
    """interval_days=8 is still out of range."""
    payload = {"mode": "interval", "interval_days": 8, "anchor_date": ""}
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 400


def test_set_schedule_interval_has_deadline_false(client):
    """interval_has_deadline=False is accepted and persisted."""
    payload = {
        "mode": "interval",
        "interval_days": 2,
        "anchor_date": "",
        "interval_has_deadline": False,
        "interval_hour": 0,
        "interval_minute": 0,
    }
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200
    assert resp.get_json()["interval_has_deadline"] is False


def test_set_schedule_interval_anchor_date_empty_string(client):
    """anchor_date empty string is valid (means use today)."""
    payload = {"mode": "interval", "interval_days": 2, "anchor_date": ""}
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200
    assert resp.get_json()["anchor_date"] == ""


def test_set_schedule_invalid_mode(client):
    payload = {"mode": "weekly", "day_configs": []}
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 400


def test_set_schedule_upserts_existing(client):
    # Set once with days mode
    client.put(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule',
        json={"mode": "days", "day_configs": [{"day": 1, "hour": 8, "minute": 0}]},
    )
    # Overwrite with interval mode
    client.put(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule',
        json={"mode": "interval", "interval_days": 2, "anchor_date": "", "interval_hour": 9, "interval_minute": 0},
    )
    resp = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp.status_code == 200
    assert resp.get_json()["mode"] == "interval"


def test_old_record_missing_new_fields_loads_with_defaults(client):
    """Old DB records without anchor_date/interval_has_deadline load with correct defaults."""
    from db.chore_schedules import upsert_schedule
    from models.chore_schedule import ChoreSchedule

    # Insert a schedule as if it was created before phase 2 (missing new fields)
    old_style = ChoreSchedule(
        child_id=TEST_CHILD_ID,
        task_id=TEST_TASK_ID,
        mode='interval',
        interval_days=3,
        anchor_date='',           # default value
        interval_has_deadline=True,  # default value
        interval_hour=8,
        interval_minute=0,
    )
    upsert_schedule(old_style)

    # Manually wipe the new fields from the raw stored record to simulate a pre-phase-2 record
    from db.db import chore_schedules_db
    from tinydb import Query
    ScheduleQ = Query()
    record = chore_schedules_db.search(
        (ScheduleQ.child_id == TEST_CHILD_ID) & (ScheduleQ.task_id == TEST_TASK_ID)
    )[0]
    doc_id = chore_schedules_db.get(
        (ScheduleQ.child_id == TEST_CHILD_ID) & (ScheduleQ.task_id == TEST_TASK_ID)
    ).doc_id
    chore_schedules_db.update(
        lambda rec: (
            rec.pop('anchor_date', None),
            rec.pop('interval_has_deadline', None),
        ),
        doc_ids=[doc_id],
    )

    resp = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp.status_code == 200
    data = resp.get_json()
    assert data['anchor_date'] == ''
    assert data['interval_has_deadline'] is True


# ---------------------------------------------------------------------------
# DELETE schedule
# ---------------------------------------------------------------------------

def test_delete_schedule(client):
    client.put(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule',
        json={"mode": "days", "day_configs": []},
    )
    resp = client.delete(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp.status_code == 200

    # Verify gone
    resp2 = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp2.status_code == 404


def test_delete_schedule_not_found(client):
    chore_schedules_db.truncate()
    resp = client.delete(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# POST extend
# ---------------------------------------------------------------------------

def test_extend_chore_time(client):
    resp = client.post(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/extend',
        json={"date": "2025-01-15"},
    )
    assert resp.status_code == 200
    data = resp.get_json()
    assert data["child_id"] == TEST_CHILD_ID
    assert data["task_id"] == TEST_TASK_ID
    assert data["date"] == "2025-01-15"


def test_extend_chore_time_duplicate_returns_409(client):
    task_extensions_db.truncate()
    client.post(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/extend',
        json={"date": "2025-01-15"},
    )
    resp = client.post(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/extend',
        json={"date": "2025-01-15"},
    )
    assert resp.status_code == 409


def test_extend_chore_time_different_dates_allowed(client):
    task_extensions_db.truncate()
    r1 = client.post(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/extend',
        json={"date": "2025-01-15"},
    )
    r2 = client.post(
        f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/extend',
        json={"date": "2025-01-16"},
    )
    assert r1.status_code == 200
    assert r2.status_code == 200


def test_extend_chore_time_missing_date(client):
    resp = client.post(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/extend', json={})
    assert resp.status_code == 400


def test_extend_chore_time_bad_child(client):
    resp = client.post('/child/bad-child/task/bad-task/extend', json={"date": "2025-01-15"})
    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# enabled field
# ---------------------------------------------------------------------------

def test_create_schedule_enabled_by_default(client):
    """Omitting 'enabled' should persist as True."""
    payload = {"mode": "days", "day_configs": [{"day": 1, "hour": 8, "minute": 0}]}
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200
    assert resp.get_json()["enabled"] is True


def test_create_schedule_with_enabled_false(client):
    """Explicitly setting enabled=False persists correctly."""
    payload = {
        "mode": "days",
        "day_configs": [{"day": 1, "hour": 8, "minute": 0}],
        "enabled": False,
    }
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200
    assert resp.get_json()["enabled"] is False


def test_update_schedule_toggle_enabled(client):
    """Toggle enabled off, then back on; both changes persist."""
    base = {"mode": "days", "day_configs": [{"day": 2, "hour": 9, "minute": 0}]}

    r1 = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json={**base, "enabled": True})
    assert r1.get_json()["enabled"] is True

    r2 = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json={**base, "enabled": False})
    assert r2.get_json()["enabled"] is False

    r3 = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json={**base, "enabled": True})
    assert r3.get_json()["enabled"] is True


def test_enabled_field_in_get_response(client):
    """GET response includes the 'enabled' field."""
    payload = {"mode": "interval", "interval_days": 2, "anchor_date": "", "enabled": False}
    client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)

    resp = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
    assert resp.status_code == 200
    data = resp.get_json()
    assert "enabled" in data
    assert data["enabled"] is False


def test_enabled_invalid_value_returns_400(client):
    """Non-boolean value for enabled returns 400."""
    payload = {"mode": "days", "day_configs": [], "enabled": "yes"}
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 400


# ---------------------------------------------------------------------------
# ChoreSchedule model: enabled serialization
# ---------------------------------------------------------------------------

def test_chore_schedule_from_dict_defaults_enabled():
    """from_dict without 'enabled' defaults to True."""
    from models.chore_schedule import ChoreSchedule
    s = ChoreSchedule.from_dict({
        "child_id": "c1", "task_id": "t1", "mode": "days",
        "day_configs": [], "interval_days": 2, "anchor_date": "",
        "interval_has_deadline": True, "interval_hour": 0, "interval_minute": 0,
    })
    assert s.enabled is True


def test_chore_schedule_from_dict_enabled_false():
    """from_dict with enabled=False preserves the value."""
    from models.chore_schedule import ChoreSchedule
    s = ChoreSchedule.from_dict({
        "child_id": "c1", "task_id": "t1", "mode": "days",
        "day_configs": [], "interval_days": 2, "anchor_date": "",
        "interval_has_deadline": True, "interval_hour": 0, "interval_minute": 0,
        "enabled": False,
    })
    assert s.enabled is False


def test_chore_schedule_to_dict_includes_enabled():
    """to_dict includes the 'enabled' key with the correct value."""
    from models.chore_schedule import ChoreSchedule
    s = ChoreSchedule(child_id="c1", task_id="t1", mode="days", enabled=False)
    d = s.to_dict()
    assert "enabled" in d
    assert d["enabled"] is False


# ---------------------------------------------------------------------------
# Bug #10: Setting schedule resets pending confirmations
# ---------------------------------------------------------------------------

def test_set_schedule_resets_pending_confirmation(client):
    """Setting/updating a chore schedule should remove pending chore confirmations."""
    chore_schedules_db.truncate()
    pending_confirmations_db.truncate()
    task_db.truncate()

    task_db.insert({
        'id': TEST_TASK_ID, 'name': 'Test Chore', 'points': 5,
        'type': 'chore', 'user_id': TEST_USER_ID
    })
    pending_confirmations_db.insert(PendingConfirmation(
        child_id=TEST_CHILD_ID, entity_id=TEST_TASK_ID,
        entity_type='chore', user_id=TEST_USER_ID
    ).to_dict())

    payload = {
        "mode": "days",
        "day_configs": [{"day": 1, "hour": 8, "minute": 0}],
    }
    resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
    assert resp.status_code == 200

    PQ = Query()
    conf = pending_confirmations_db.get(
        (PQ.child_id == TEST_CHILD_ID) & (PQ.entity_id == TEST_TASK_ID) & (PQ.entity_type == 'chore')
    )
    assert conf is None
