from __future__ import annotations

from datetime import date
from decimal import Decimal

import pytest

from quartermaster import month_service, service
from quartermaster.models import MonthState, Section


def _seed(db, balance_to_zero=False):
    service.add_entry(db, Section.income, "Paycheck", Decimal("1000.00"))
    service.add_entry(db, Section.fixed_bill, "Rent", Decimal("600.00"))
    service.add_entry(db, Section.debt_minimum, "Card A", Decimal("40.00"))
    service.add_entry(db, Section.food, "Groceries", Decimal("300.00"))
    service.add_entry(db, Section.other, "Misc", Decimal("60.00"))
    month = month_service.create_month(db, "2026-04")
    if balance_to_zero:
        # Post one transaction per entry matching the planned amount
        for entry in month.entries:
            month_service.add_posting(
                db, month, entry.id, date.today(), entry.planned, description="seed"
            )
        db.refresh(month)
    return month


def test_create_month_defaults_to_planning(db):
    _seed(db)
    month = month_service.get_month(db, "2026-04")
    assert month is not None
    assert month.state == MonthState.planning
    assert month.activated_at is None
    assert month.closed_at is None


def test_activate_transitions_to_active(db):
    month = _seed(db)
    activated = month_service.activate_month(db, month)
    assert activated.state == MonthState.active
    assert activated.activated_at is not None


def test_activate_rejects_non_planning_month(db):
    month = _seed(db)
    month_service.activate_month(db, month)
    with pytest.raises(month_service.MonthLifecycleError):
        month_service.activate_month(db, month)


def test_close_requires_active(db):
    month = _seed(db)
    # still planning; close should reject
    with pytest.raises(month_service.MonthLifecycleError):
        month_service.close_month(db, month)


def test_close_requires_zero_balance(db):
    month = _seed(db)
    month_service.activate_month(db, month)
    # applied zero is 0 - 0 = 0, but income applied is 0 so
    # (0 income - 0 others) == 0 actually. Force a non-zero by applying to food.
    food = next(e for e in month.entries if e.origin_name == "Groceries")
    month_service.add_posting(
        db, month, food.id, date.today(), Decimal("50.00"), description="seed"
    )
    db.refresh(month)
    with pytest.raises(month_service.MonthLifecycleError) as exc:
        month_service.close_month(db, month)
    assert "0.00" in str(exc.value)


def test_close_succeeds_when_balanced(db):
    month = _seed(db, balance_to_zero=True)
    month_service.activate_month(db, month)
    closed = month_service.close_month(db, month)
    assert closed.state == MonthState.closed
    assert closed.closed_at is not None


def test_reopen_from_closed(db):
    month = _seed(db, balance_to_zero=True)
    month_service.activate_month(db, month)
    month_service.close_month(db, month)
    reopened = month_service.reopen_month(db, month)
    assert reopened.state == MonthState.active
    assert reopened.closed_at is None


def test_reopen_rejects_non_closed(db):
    month = _seed(db)
    with pytest.raises(month_service.MonthLifecycleError):
        month_service.reopen_month(db, month)


def test_ensure_editable_rejects_closed(db):
    month = _seed(db, balance_to_zero=True)
    month_service.activate_month(db, month)
    month_service.close_month(db, month)
    with pytest.raises(month_service.MonthLifecycleError):
        month_service.ensure_editable(month)


def test_ensure_editable_allows_planning_and_active(db):
    month = _seed(db)
    month_service.ensure_editable(month)  # planning: fine
    month_service.activate_month(db, month)
    month_service.ensure_editable(month)  # active: fine


# --- route-level -----------------------------------------------------------


def _seed_via_api(client, balance_to_zero=False):
    client.post(
        "/sections/income/entries",
        data={"name": "Paycheck", "amount": "1000.00"},
    )
    client.post(
        "/sections/fixed_bill/entries",
        data={"name": "Rent", "amount": "600.00"},
    )
    client.post(
        "/sections/food/entries",
        data={"name": "Groceries", "amount": "400.00"},
    )
    client.post("/month/2026-04/create")
    if balance_to_zero:
        # entry ids 1,2,3 in that order
        client.post(
            "/month/2026-04/entries/1/postings",
            data={"occurred_on": "2026-04-01", "amount": "1000.00"},
        )
        client.post(
            "/month/2026-04/entries/2/postings",
            data={"occurred_on": "2026-04-01", "amount": "600.00"},
        )
        client.post(
            "/month/2026-04/entries/3/postings",
            data={"occurred_on": "2026-04-15", "amount": "400.00"},
        )


def test_activate_route(client):
    _seed_via_api(client)
    response = client.post("/month/2026-04/activate")
    assert response.status_code == 204
    assert response.headers.get("hx-redirect") == "/month/2026-04"


def test_close_route_rejects_unbalanced(client):
    _seed_via_api(client)
    client.post("/month/2026-04/activate")
    # post an expense without income to produce an unbalanced state
    client.post(
        "/month/2026-04/entries/2/postings",
        data={"occurred_on": "2026-04-01", "amount": "100.00"},
    )
    response = client.post("/month/2026-04/close")
    assert response.status_code == 400
    assert "0.00" in response.json()["detail"]


def test_close_route_succeeds_balanced(client):
    _seed_via_api(client, balance_to_zero=True)
    client.post("/month/2026-04/activate")
    response = client.post("/month/2026-04/close")
    assert response.status_code == 204
    assert response.headers.get("hx-redirect") == "/month/2026-04"


def test_reopen_route(client):
    _seed_via_api(client, balance_to_zero=True)
    client.post("/month/2026-04/activate")
    client.post("/month/2026-04/close")
    response = client.post("/month/2026-04/reopen")
    assert response.status_code == 204


def test_mutations_rejected_on_closed_month(client):
    _seed_via_api(client, balance_to_zero=True)
    client.post("/month/2026-04/activate")
    client.post("/month/2026-04/close")
    # try to add, update, delete, and re-set target
    add_response = client.post(
        "/month/2026-04/sections/other/entries",
        data={"name": "Nope", "planned": "10.00"},
    )
    assert add_response.status_code == 400
    update_response = client.post(
        "/month/2026-04/entries/1/postings",
        data={"occurred_on": "2026-04-15", "amount": "50.00"},
    )
    assert update_response.status_code == 400
    delete_response = client.delete("/month/2026-04/entries/1")
    assert delete_response.status_code == 400


def test_month_page_shows_planning_badge_and_activate_button(client):
    _seed_via_api(client)
    response = client.get("/month/2026-04")
    assert response.status_code == 200
    assert "state-planning" in response.text
    assert "/activate" in response.text
    assert "Activate" in response.text


def test_month_page_shows_active_badge_with_disabled_close_when_unbalanced(client):
    _seed_via_api(client)
    client.post("/month/2026-04/activate")
    # force imbalance via a posting
    client.post(
        "/month/2026-04/entries/2/postings",
        data={"occurred_on": "2026-04-01", "amount": "100.00"},
    )
    response = client.get("/month/2026-04")
    assert response.status_code == 200
    assert "state-active" in response.text
    assert "/close" in response.text
    assert "disabled" in response.text


def test_month_page_shows_active_badge_with_enabled_close_when_balanced(client):
    _seed_via_api(client, balance_to_zero=True)
    client.post("/month/2026-04/activate")
    response = client.get("/month/2026-04")
    assert "state-active" in response.text
    assert "/close" in response.text
    # Close button is present without the disabled attribute on the button itself
    assert 'type="submit"' in response.text


def test_month_page_shows_closed_badge_and_reopen_button(client):
    _seed_via_api(client, balance_to_zero=True)
    client.post("/month/2026-04/activate")
    client.post("/month/2026-04/close")
    response = client.get("/month/2026-04")
    assert "state-closed" in response.text
    assert "/reopen" in response.text
    assert "Closed" in response.text
    # add forms hidden on closed month
    assert "hx-post=\"/month/2026-04/sections/" not in response.text


def test_closed_month_inputs_are_disabled(client):
    _seed_via_api(client, balance_to_zero=True)
    client.post("/month/2026-04/activate")
    client.post("/month/2026-04/close")
    response = client.get("/month/2026-04")
    # Editable inputs are replaced by readonly spans on closed months,
    # so the name/planned inputs should not appear.
    assert 'name="planned"' not in response.text
    assert 'name="name"' not in response.text
    assert "class=\"readonly\"" in response.text
    # delete buttons not rendered
    assert "Delete Paycheck" not in response.text
