"""Phase 6.2 — Enterprise Sales Process Engine tests."""
from __future__ import annotations

from datetime import date
from decimal import Decimal
from uuid import uuid4

import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from app.core.database import engine
from app.events.publisher import get_event_publisher
from app.events.types import CrmEventType
from app.models.sales_engine import (
    OpportunityProduct,
    SalesForecast,
    SalesPlaybook,
    SalesStageHistory,
)
from app.models.types import (
    ContactRoleType,
    ForecastKind,
    ForecastPeriod,
    GoalMetric,
    GoalScope,
    PlaybookStepKind,
    TargetPeriod,
    WinLossOutcome,
)
from app.schemas.sales_engine import (
    ForecastComputeRequest,
    GoalCreate,
    OpportunityCompetitorCreate,
    OpportunityContactCreate,
    OpportunityCreate,
    OpportunityLoseRequest,
    OpportunityProductCreate,
    OpportunityStageChangeRequest,
    OpportunityUpdate,
    OpportunityWinRequest,
    PipelineCreate,
    PipelineStageCreate,
    PlaybookAssignRequest,
    PlaybookCompleteStepRequest,
    PlaybookCreate,
    PlaybookStepCreate,
    TargetCreate,
    WinLossCategoryCreate,
    WinLossReasonCreate,
)
from app.services.opportunity_service import OpportunityService, PipelineService
from app.services.sales_engine_services import (
    ForecastService,
    GoalService,
    OpportunityExtrasService,
    PlaybookService,
    TargetService,
    WinLossService,
)
from app.tests.conftest import TENANT_A, TENANT_B, tenant_headers
from app.validators.sales_engine import (
    validate_lost_requires_reason,
    validate_stage_flags,
)
from shared.exceptions import AppError


def _session_factory():
    return async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)


async def _seed_pipeline(session, tenant_id):
    ps = PipelineService(session)
    pipe = await ps.create(
        tenant_id,
        PipelineCreate(
            name="Enterprise", code=f"ENT-{uuid4().hex[:6]}", is_default=True
        ),
    )
    start = await ps.create_stage(
        tenant_id,
        PipelineStageCreate(
            pipeline_id=pipe.id,
            name="Qualify",
            code="START",
            position=0,
            win_probability=10,
            is_start_stage=True,
            color="#00AA00",
        ),
    )
    mid = await ps.create_stage(
        tenant_id,
        PipelineStageCreate(
            pipeline_id=pipe.id,
            name="Negotiate",
            code="MID",
            position=1,
            win_probability=50,
        ),
    )
    won = await ps.create_stage(
        tenant_id,
        PipelineStageCreate(
            pipeline_id=pipe.id,
            name="Closed Won",
            code="WON",
            position=2,
            win_probability=100,
            is_won_stage=True,
            is_closed_stage=True,
        ),
    )
    lost = await ps.create_stage(
        tenant_id,
        PipelineStageCreate(
            pipeline_id=pipe.id,
            name="Closed Lost",
            code="LOST",
            position=3,
            win_probability=0,
            is_lost_stage=True,
            is_closed_stage=True,
        ),
    )
    return pipe, start, mid, won, lost


@pytest.mark.asyncio
async def test_pipeline_stage_rules_and_opportunity_number(db_setup):
    Session = _session_factory()
    async with Session() as session:
        pipe, start, mid, won, lost = await _seed_pipeline(session, TENANT_A)
        ps = PipelineService(session)
        with pytest.raises(AppError) as exc:
            await ps.create_stage(
                TENANT_A,
                PipelineStageCreate(
                    pipeline_id=pipe.id,
                    name="Another Start",
                    code="START2",
                    is_start_stage=True,
                ),
            )
        assert exc.value.error_code == "duplicate_start_stage"

        opp = await OpportunityService(session).create(
            TENANT_A,
            OpportunityCreate(
                name="Deal A",
                pipeline_id=pipe.id,
                stage_id=start.id,
                amount=Decimal("1000"),
                expected_close_date=date(2026, 8, 1),
            ),
        )
        assert opp.opportunity_number.startswith("OPP-")
        assert opp.title == "Deal A"
        assert opp.probability == 10


@pytest.mark.asyncio
async def test_stage_change_history_and_immutable_won(db_setup):
    Session = _session_factory()
    async with Session() as session:
        pipe, start, mid, won, lost = await _seed_pipeline(session, TENANT_A)
        svc = OpportunityService(session)
        opp = await svc.create(
            TENANT_A,
            OpportunityCreate(
                name="Deal B",
                pipeline_id=pipe.id,
                stage_id=start.id,
                amount=Decimal("5000"),
            ),
        )
        moved = await svc.change_stage(
            TENANT_A,
            opp.id,
            OpportunityStageChangeRequest(stage_id=mid.id, reason="progress"),
        )
        assert moved.stage_id == mid.id
        assert moved.probability == 50
        result = await session.execute(
            select(SalesStageHistory).where(SalesStageHistory.opportunity_id == opp.id)
        )
        assert len(list(result.scalars().all())) >= 2

        await svc.mark_won(
            TENANT_A, opp.id, OpportunityWinRequest(custom_reason="best price")
        )
        with pytest.raises(AppError) as exc:
            await svc.update(
                TENANT_A, opp.id, OpportunityUpdate(name="nope")
            )
        assert exc.value.error_code == "opportunity_immutable"


@pytest.mark.asyncio
async def test_lost_requires_reason_and_win_loss(db_setup):
    Session = _session_factory()
    async with Session() as session:
        pipe, start, mid, won, lost = await _seed_pipeline(session, TENANT_A)
        svc = OpportunityService(session)
        opp = await svc.create(
            TENANT_A,
            OpportunityCreate(
                name="Deal C",
                pipeline_id=pipe.id,
                stage_id=start.id,
                amount=Decimal("200"),
            ),
        )
        wl = WinLossService(session)
        cat = await wl.create_category(
            TENANT_A, WinLossCategoryCreate(code="PRICE", name="Price")
        )
        reason = await wl.create_reason(
            TENANT_A,
            WinLossReasonCreate(
                code="TOO_HIGH",
                name="Too expensive",
                outcome=WinLossOutcome.LOST,
                category_id=cat.id,
            ),
        )
        with pytest.raises(AppError):
            validate_lost_requires_reason(None, None)
        lost_opp = await svc.mark_lost(
            TENANT_A,
            opp.id,
            OpportunityLoseRequest(reason_id=reason.id, price_factor=True),
        )
        assert lost_opp.status.value == "lost"
        events = get_event_publisher().published
        assert any(e.event_type == CrmEventType.OPPORTUNITY_LOST.value for e in events)


@pytest.mark.asyncio
async def test_playbook_forecast_goals_products(db_setup):
    Session = _session_factory()
    async with Session() as session:
        pipe, start, mid, won, lost = await _seed_pipeline(session, TENANT_A)
        opp = await OpportunityService(session).create(
            TENANT_A,
            OpportunityCreate(
                name="Deal D",
                pipeline_id=pipe.id,
                stage_id=mid.id,
                amount=Decimal("10000"),
                expected_revenue=Decimal("10000"),
                probability=80,
                expected_close_date=date(2026, 9, 15),
            ),
        )
        pb = PlaybookService(session)
        book = await pb.create(
            TENANT_A,
            PlaybookCreate(
                name="Enterprise Playbook",
                code="ENT-PB",
                version=1,
                pipeline_id=pipe.id,
            ),
        )
        step = await pb.add_step(
            TENANT_A,
            PlaybookStepCreate(
                playbook_id=book.id,
                title="Discovery",
                step_kind=PlaybookStepKind.REQUIRED_ACTIVITY,
                is_required=True,
                checklist_items=["agenda", "stakeholders"],
            ),
        )
        assignment = await pb.assign(
            TENANT_A,
            PlaybookAssignRequest(playbook_id=book.id, opportunity_id=opp.id),
        )
        done = await pb.complete_step(
            TENANT_A,
            assignment.id,
            PlaybookCompleteStepRequest(step_id=step.id),
        )
        assert done.completion_percent == 100
        assert done.status.value == "completed"
        assert any(
            e.event_type == CrmEventType.PLAYBOOK_COMPLETED.value
            for e in get_event_publisher().published
        )

        extras = OpportunityExtrasService(session)
        product = await extras.add_product(
            TENANT_A,
            OpportunityProductCreate(
                opportunity_id=opp.id,
                product_ref_id="prod-sku-1",
                quantity=Decimal("2"),
                unit_price=Decimal("100"),
                discount_percent=Decimal("10"),
            ),
        )
        assert product.expected_revenue == Decimal("180")
        await extras.add_contact(
            TENANT_A,
            OpportunityContactCreate(
                opportunity_id=opp.id,
                contact_id=uuid4(),
                role=ContactRoleType.DECISION_MAKER,
                is_primary=True,
            ),
        )
        await extras.add_competitor(
            TENANT_A,
            OpportunityCompetitorCreate(
                opportunity_id=opp.id,
                competitor_name="RivalCo",
                strength="price",
                weakness="support",
                win_probability=30,
            ),
        )

        forecast = await ForecastService(session).compute(
            TENANT_A,
            ForecastComputeRequest(
                name="Q3 Weighted",
                kind=ForecastKind.WEIGHTED,
                period=ForecastPeriod.QUARTERLY,
                period_start=date(2026, 7, 1),
                period_end=date(2026, 9, 30),
                pipeline_id=pipe.id,
            ),
        )
        assert forecast.opportunity_count >= 1
        assert forecast.weighted_amount > 0
        assert forecast.committed_amount > 0

        goal = await GoalService(session).create(
            TENANT_A,
            GoalCreate(
                name="Team Revenue",
                scope=GoalScope.TEAM,
                metric=GoalMetric.REVENUE,
                team_key="sales-ent",
                target_value=Decimal("100000"),
                period_start=date(2026, 1, 1),
                period_end=date(2026, 12, 31),
            ),
        )
        target = await TargetService(session).create(
            TENANT_A,
            TargetCreate(
                name="Q3 Target",
                period=TargetPeriod.QUARTERLY,
                period_start=date(2026, 7, 1),
                period_end=date(2026, 9, 30),
                goal_id=goal.id,
                target_value=Decimal("25000"),
                actual_value=Decimal("12500"),
            ),
        )
        assert target.achievement_percent == Decimal("50.0000")


@pytest.mark.asyncio
async def test_sales_engine_api_flow(client):
    pipeline = await client.post(
        "/api/v1/pipelines",
        json={"name": "Retail", "code": "RETAIL62", "sort_order": 1},
        headers=tenant_headers(TENANT_A),
    )
    assert pipeline.status_code == 201, pipeline.text
    pipeline_id = pipeline.json()["id"]

    stage = await client.post(
        "/api/v1/pipelines/stages",
        json={
            "pipeline_id": pipeline_id,
            "name": "Open",
            "code": "OPEN",
            "is_start_stage": True,
            "win_probability": 20,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert stage.status_code == 201
    stage_id = stage.json()["id"]

    opp = await client.post(
        "/api/v1/opportunities",
        json={
            "title": "Retail Deal",
            "pipeline_id": pipeline_id,
            "stage_id": stage_id,
            "amount": "8000",
            "expected_close_date": "2026-10-01",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert opp.status_code == 201, opp.text
    assert opp.json()["opportunity_number"]
    opportunity_id = opp.json()["id"]

    stage2 = await client.post(
        "/api/v1/pipelines/stages",
        json={
            "pipeline_id": pipeline_id,
            "name": "Proposal",
            "code": "PROP",
            "position": 1,
            "win_probability": 55,
        },
        headers=tenant_headers(TENANT_A),
    )
    changed = await client.post(
        f"/api/v1/opportunities/{opportunity_id}/stage",
        json={"stage_id": stage2.json()["id"], "reason": "ready"},
        headers=tenant_headers(TENANT_A),
    )
    assert changed.status_code == 200
    hist = await client.get(
        f"/api/v1/opportunities/{opportunity_id}/stage-history",
        headers=tenant_headers(TENANT_A),
    )
    assert hist.status_code == 200
    assert len(hist.json()) >= 2

    pb = await client.post(
        "/api/v1/playbooks",
        json={"name": "Retail PB", "code": "RET-PB", "pipeline_id": pipeline_id},
        headers=tenant_headers(TENANT_A),
    )
    assert pb.status_code == 201
    await client.post(
        "/api/v1/playbooks/steps",
        json={
            "playbook_id": pb.json()["id"],
            "title": "Call",
            "step_kind": "recommended_activity",
        },
        headers=tenant_headers(TENANT_A),
    )
    assign = await client.post(
        "/api/v1/playbooks/assignments",
        json={"playbook_id": pb.json()["id"], "opportunity_id": opportunity_id},
        headers=tenant_headers(TENANT_A),
    )
    assert assign.status_code == 201

    prod = await client.post(
        "/api/v1/opportunity-products",
        json={
            "opportunity_id": opportunity_id,
            "product_ref_id": "SKU-9",
            "quantity": "1",
            "unit_price": "8000",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert prod.status_code == 201

    fc = await client.post(
        "/api/v1/forecasts/compute",
        json={
            "name": "Oct",
            "period": "monthly",
            "period_start": "2026-10-01",
            "period_end": "2026-10-31",
            "pipeline_id": pipeline_id,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert fc.status_code == 201, fc.text

    goal = await client.post(
        "/api/v1/goals",
        json={
            "name": "Indiv",
            "scope": "individual",
            "metric": "opportunity_count",
            "owner_user_id": "u1",
            "target_value": "10",
            "period_start": "2026-01-01",
            "period_end": "2026-12-31",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert goal.status_code == 201

    target = await client.post(
        "/api/v1/targets",
        json={
            "name": "Annual",
            "period": "annual",
            "period_start": "2026-01-01",
            "period_end": "2026-12-31",
            "target_value": "100",
            "actual_value": "40",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert target.status_code == 201
    assert float(target.json()["achievement_percent"]) == 40.0

    lost = await client.post(
        f"/api/v1/opportunities/{opportunity_id}/lose",
        json={"custom_reason": "budget cut", "timing_factor": True},
        headers=tenant_headers(TENANT_A),
    )
    assert lost.status_code == 200
    assert lost.json()["status"] == "lost"
    wl = await client.get(
        f"/api/v1/win-loss/opportunities/{opportunity_id}",
        headers=tenant_headers(TENANT_A),
    )
    assert wl.status_code == 200
    assert wl.json()["is_locked"] is True

    other = await client.get(
        "/api/v1/playbooks",
        headers=tenant_headers(TENANT_B),
    )
    assert other.status_code == 200
    assert other.json() == []


def test_stage_flag_validator():
    with pytest.raises(AppError):
        validate_stage_flags(
            is_start_stage=True, is_won_stage=True, is_lost_stage=False
        )


def test_sales_engine_models_have_tenant():
    assert hasattr(SalesPlaybook, "tenant_id")
    assert hasattr(OpportunityProduct, "tenant_id")
    assert hasattr(SalesForecast, "tenant_id")
    assert hasattr(SalesStageHistory, "tenant_id")
