"""PlusROI Projects write service (services/projects_plus.py; phase-3
slice 2a).

SQLite fixtures per the house style (mirrors tests/clients): an app-owned
PlusROI tenant (writable) with a seeded client carrying commercial
defaults, a second app-owned tenant for isolation, a sheet-owned tenant
for the write-block, and SimpleNamespace owner/worker actors.
"""

import uuid
from types import SimpleNamespace

import pytest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from app.models import (
    Base,
    Party,
    Project,
    ProjectBilling,
    ProjectMilestone,
    Tenant,
)
from app.services import clients as clients_svc
from app.services import projects_plus as svc
from app.services.errors import ServiceError


def pid(raw: str) -> uuid.UUID:
    return uuid.UUID(raw)


@pytest.fixture()
def s():
    engine = create_engine(
        "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
    )
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        yield session
    engine.dispose()


def _tenant(s, name, slug, settings):
    party = Party(kind="org", name=name, slug=slug)
    s.add(party)
    s.flush()
    tenant = Tenant(party_id=party.id, settings=settings)
    s.add(tenant)
    s.flush()
    return tenant, party


APP = {"books_owner": "app", "default_currency": "CAD"}
SHEET = {"books_owner": "sheet"}


def owner(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        is_viewing_as=False,
        allowed=lambda cap: True,
    )


def worker(tenant_id):
    return SimpleNamespace(
        tenant_id=tenant_id,
        user_id=None,
        real_user_id=None,
        is_viewing_as=False,
        allowed=lambda cap: False,
    )


@pytest.fixture()
def fx(s):
    plusroi, plusroi_party = _tenant(s, "PlusROI", "plusroi", APP)
    other, other_party = _tenant(s, "Other Books", "other", APP)
    sheet, sheet_party = _tenant(s, "SheetCo", "sheetco", SHEET)
    s.flush()
    # a client of PlusROI carrying inheritable commercial defaults
    client = clients_svc.create_client(
        s,
        owner(plusroi.id),
        {
            "name": "Copernic",
            "currency": "USD",
            "terms": "Net 30",
            "invoice_timing": "month_end",
            "default_commission_pct": "10",
        },
    )
    return SimpleNamespace(
        plusroi=plusroi,
        plusroi_party=plusroi_party,
        other=other,
        sheet=sheet,
        client_id=client["row"]["party_id"],
    )


def _base(fx, **over):
    body = {"client_party_id": fx.client_id, "name": "Website"}
    body.update(over)
    return body


# --------------------------------------------------------------- create: recurring


def test_create_recurring_roundtrip(s, fx):
    res = svc.create_project(
        s,
        owner(fx.plusroi.id),
        _base(
            fx,
            name="Monthly PPC",
            is_recurring=True,
            cycle_months=1,
            billing_timing="mid_month",
            amount="1500",
        ),
    )
    assert res["ok"] is True
    row = res["row"]
    assert row["is_recurring"] is True
    assert row["cycle_months"] == 1
    assert row["billing_timing"] == "mid_month"
    assert row["amount"] == "1500.00"
    assert row["milestone_count"] == 0
    # currency/terms/commission INHERITED from the client profile
    assert row["currency"] == "USD"
    assert row["terms"] == "Net 30"
    assert row["default_commission_pct"] == "10.0000"

    project = s.get(Project, pid(row["project_id"]))
    assert project.operator_party_id == fx.plusroi_party.id
    billing = s.get(ProjectBilling, project.id)
    assert billing.is_recurring is True and str(billing.amount) == "1500.00"
    assert billing.billing_type == "recurring"
    assert (
        s.scalar(
            select(func.count()).select_from(ProjectMilestone).where(
                ProjectMilestone.project_id == project.id
            )
        )
        == 0
    )


def test_create_recurring_requires_amount(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_project(
            s,
            owner(fx.plusroi.id),
            _base(fx, is_recurring=True, cycle_months=1, billing_timing="month_end"),
        )
    assert err.value.code == "AMOUNT_REQUIRED"


def test_create_recurring_requires_timing(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_project(
            s,
            owner(fx.plusroi.id),
            _base(fx, is_recurring=True, cycle_months=1, amount="1500"),
        )
    assert err.value.code == "TIMING_REQUIRED"


def test_create_recurring_bad_timing(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_project(
            s,
            owner(fx.plusroi.id),
            _base(fx, is_recurring=True, billing_timing="whenever", amount="1"),
        )
    assert err.value.code == "BAD_TIMING"


# --------------------------------------------------------------- create: milestones


def test_create_milestones_roundtrip(s, fx):
    res = svc.create_project(
        s,
        owner(fx.plusroi.id),
        _base(
            fx,
            name="Website Rebuild",
            currency="CAD",  # override the USD inheritance
            is_recurring=False,
            milestones=[
                {"label": "Deposit", "amount": "1000"},
                {"label": "Milestone", "amount": "2000"},
                {"label": "On completion", "amount": "2000"},
            ],
        ),
    )
    row = res["row"]
    assert row["is_recurring"] is False
    assert row["currency"] == "CAD"  # override won
    assert row["milestone_count"] == 3
    assert row["milestones_total"] == "5000.00"
    assert row["amount"] is None  # non-recurring leaves the recurring amount null

    ms = s.scalars(
        select(ProjectMilestone)
        .where(ProjectMilestone.project_id == pid(row["project_id"]))
        .order_by(ProjectMilestone.sort_order)
    ).all()
    assert [m.label for m in ms] == ["Deposit", "Milestone", "On completion"]
    assert [m.sort_order for m in ms] == [0, 1, 2]
    assert all(m.currency == "CAD" for m in ms)


def test_create_non_recurring_requires_a_milestone(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_project(s, owner(fx.plusroi.id), _base(fx, is_recurring=False, milestones=[]))
    assert err.value.code == "MILESTONES_REQUIRED"


def test_create_milestone_blank_row_ignored_but_needs_one(s, fx):
    # a fully-blank repeater row is dropped; a half-filled one is an error
    with pytest.raises(ServiceError) as err:
        svc.create_project(
            s,
            owner(fx.plusroi.id),
            _base(fx, milestones=[{"label": "Deposit", "amount": ""}]),
        )
    assert err.value.code == "MILESTONE_AMOUNT_REQUIRED"


# --------------------------------------------------------------- inheritance


def test_defaults_inherited_when_blank(s, fx):
    res = svc.create_project(
        s, owner(fx.plusroi.id), _base(fx, milestones=[{"label": "x", "amount": "1"}])
    )
    row = res["row"]
    assert row["currency"] == "USD"  # from the client profile
    assert row["terms"] == "Net 30"
    assert row["invoice_timing"] == "month_end"
    assert row["default_commission_pct"] == "10.0000"


# --------------------------------------------------------------- uniqueness


def test_uniqueness_per_client(s, fx):
    one = _base(fx, name="Website", milestones=[{"label": "x", "amount": "1"}])
    svc.create_project(s, owner(fx.plusroi.id), one)
    with pytest.raises(ServiceError) as err:
        two = _base(fx, name="website", milestones=[{"label": "y", "amount": "2"}])
        svc.create_project(s, owner(fx.plusroi.id), two)
    assert err.value.code == "PROJECT_EXISTS" and err.value.status == 409


# --------------------------------------------------------------- gates + scope


def test_owner_gate_403(s, fx):
    for call in (
        lambda: svc.list_projects(s, worker(fx.plusroi.id)),
        lambda: svc.create_project(s, worker(fx.plusroi.id), _base(fx)),
    ):
        with pytest.raises(ServiceError) as err:
            call()
        assert err.value.code == "NOT_ALLOWED" and err.value.status == 403


def test_sheet_owned_409(s, fx):
    # the write-block fires BEFORE any client/validation load
    with pytest.raises(ServiceError) as err:
        svc.create_project(s, owner(fx.sheet.id), {"name": "X", "client_party_id": fx.client_id})
    assert err.value.code == "SHEET_OWNED_READ_ONLY" and err.value.status == 409


def test_client_must_be_this_tenants(s, fx):
    # PlusROI's client is NOT a client of the other (app-owned) tenant
    body = _base(fx, milestones=[{"label": "x", "amount": "1"}])
    with pytest.raises(ServiceError) as err:
        svc.create_project(s, owner(fx.other.id), body)
    assert err.value.code == "CLIENT_NOT_FOUND" and err.value.status == 404


def test_client_required(s, fx):
    with pytest.raises(ServiceError) as err:
        svc.create_project(s, owner(fx.plusroi.id), {"name": "X"})
    assert err.value.code == "CLIENT_REQUIRED"


# --------------------------------------------------------------- list + get


def test_list_and_filters(s, fx):
    svc.create_project(
        s,
        owner(fx.plusroi.id),
        _base(fx, name="PPC Monthly", is_recurring=True, billing_timing="month_end", amount="1500"),
    )
    svc.create_project(
        s,
        owner(fx.plusroi.id),
        _base(fx, name="PPC MidMonth", is_recurring=True, billing_timing="mid_month", amount="900"),
    )
    svc.create_project(
        s,
        owner(fx.plusroi.id),
        _base(fx, name="Build", is_recurring=False, milestones=[{"label": "a", "amount": "5"}]),
    )

    all_rows = svc.list_projects(s, owner(fx.plusroi.id))["rows"]
    assert {r["name"] for r in all_rows} == {"PPC Monthly", "PPC MidMonth", "Build"}

    recurring = svc.list_projects(s, owner(fx.plusroi.id), recurring=True)["rows"]
    assert {r["name"] for r in recurring} == {"PPC Monthly", "PPC MidMonth"}

    month_end = svc.list_projects(
        s, owner(fx.plusroi.id), recurring=True, timing="month_end"
    )["rows"]
    assert {r["name"] for r in month_end} == {"PPC Monthly"}

    build = next(r for r in all_rows if r["name"] == "Build")
    assert build["milestone_count"] == 1 and build["milestones_total"] == "5.00"


def test_get_project_isolation(s, fx):
    res = svc.create_project(
        s, owner(fx.plusroi.id), _base(fx, milestones=[{"label": "x", "amount": "1"}])
    )
    project_id = pid(res["row"]["project_id"])
    got = svc.get_project(s, owner(fx.plusroi.id), project_id)
    assert got["project"]["name"] == "Website"
    assert len(got["milestones"]) == 1

    # the other tenant can't read it
    with pytest.raises(ServiceError) as err:
        svc.get_project(s, owner(fx.other.id), project_id)
    assert err.value.code == "PROJECT_NOT_FOUND"


# --------------------------------------------------------------- update


def test_update_identity_and_amount(s, fx):
    project_id = pid(
        svc.create_project(
            s,
            owner(fx.plusroi.id),
            _base(fx, name="PPC", is_recurring=True, billing_timing="mid_month", amount="1500"),
        )["row"]["project_id"]
    )
    # correcting THIS invoice's project default (Rob's $1,500 -> $2,000 later
    # is per-invoice; editing the project default is this path)
    res = svc.update_project(
        s, owner(fx.plusroi.id), project_id, {"name": "PPC Renamed", "amount": "2000"}
    )
    assert res["ok"] and res["changed"]
    assert res["row"]["name"] == "PPC Renamed"
    assert res["row"]["amount"] == "2000.00"


def test_update_toggle_recurring_drops_milestones(s, fx):
    project_id = pid(
        svc.create_project(
            s,
            owner(fx.plusroi.id),
            _base(fx, milestones=[{"label": "Deposit", "amount": "1000"}]),
        )["row"]["project_id"]
    )
    res = svc.update_project(
        s,
        owner(fx.plusroi.id),
        project_id,
        {"is_recurring": True, "cycle_months": 3, "billing_timing": "month_end", "amount": "500"},
    )
    assert res["row"]["is_recurring"] is True
    assert res["row"]["cycle_months"] == 3
    assert res["row"]["milestone_count"] == 0  # un-generated milestones dropped
    assert (
        s.scalar(
            select(func.count()).select_from(ProjectMilestone).where(
                ProjectMilestone.project_id == project_id
            )
        )
        == 0
    )


def test_update_replace_milestones_preserves_generated(s, fx):
    project_id = pid(
        svc.create_project(
            s,
            owner(fx.plusroi.id),
            _base(
                fx,
                milestones=[
                    {"label": "Deposit", "amount": "1000"},
                    {"label": "Final", "amount": "2000"},
                ],
            ),
        )["row"]["project_id"]
    )
    # simulate the Deposit milestone having generated an invoice
    deposit = s.scalars(
        select(ProjectMilestone).where(
            ProjectMilestone.project_id == project_id,
            ProjectMilestone.label == "Deposit",
        )
    ).one()
    deposit.generated_invoice_id = uuid.uuid4()
    s.flush()

    res = svc.update_project(
        s,
        owner(fx.plusroi.id),
        project_id,
        {"milestones": [{"label": "New milestone", "amount": "3000"}]},
    )
    # the generated Deposit is preserved; the un-generated Final is replaced
    labels = {
        m.label
        for m in s.scalars(
            select(ProjectMilestone).where(ProjectMilestone.project_id == project_id)
        ).all()
    }
    assert labels == {"Deposit", "New milestone"}
    assert res["row"]["milestone_count"] == 2


def test_update_cross_tenant_404(s, fx):
    project_id = pid(
        svc.create_project(
            s, owner(fx.plusroi.id), _base(fx, milestones=[{"label": "x", "amount": "1"}])
        )["row"]["project_id"]
    )
    with pytest.raises(ServiceError) as err:
        svc.update_project(s, owner(fx.other.id), project_id, {"name": "Z"})
    assert err.value.code == "PROJECT_NOT_FOUND"
