"""API tests for the FlowPilot Phase 4 Resolve + Escalate writeback flow.

Covers:
- Local-only path when no PSA ticket is linked (markdown stored, status flipped,
  no provider call).
- PSA post happy path (provider mocked).
- Status transition verified by re-fetch (happy path).
- Status verification failure surfaces 502 with a clear error body.
- 409 when trying to resolve an already-resolved session / escalate an
  already-escalated one.
- Escalation parallel to resolution (same structure).
"""
from __future__ import annotations

import uuid
from unittest.mock import AsyncMock, patch

import pytest
from httpx import AsyncClient

from app.api.endpoints.session_suggested_fixes import _clear_preview_cache_for_tests
from app.models.account_settings import AccountSettings
from app.models.ai_session import AISession
from app.models.psa_connection import PsaConnection
from app.services.psa.types import NoteType, PSANote, PSATicket


@pytest.fixture(autouse=True)
def _isolate_preview_cache():
    _clear_preview_cache_for_tests()
    yield
    _clear_preview_cache_for_tests()


async def _make_session(test_db, user, *, with_psa: bool = False) -> AISession:
    session_kwargs: dict = dict(
        user_id=user["user_data"]["id"],
        account_id=user["user_data"]["account_id"],
        session_type="chat",
        intake_type="free_text",
        intake_content={"text": "phase 4 test"},
        status="active",
        confidence_tier="discovery",
        conversation_messages=[],
    )
    if with_psa:
        # Fake connection — provider factory is patched in each test so we
        # never touch a real CW instance.
        from app.services.psa.encryption import encrypt_credentials
        conn = PsaConnection(
            account_id=user["user_data"]["account_id"],
            provider="connectwise",
            site_url="https://fake.cw.local",
            company_id="TEST",
            credentials_encrypted=encrypt_credentials({"public_key": "x", "private_key": "y"}),
            is_active=True,
        )
        test_db.add(conn)
        await test_db.flush()
        session_kwargs["psa_connection_id"] = conn.id
        session_kwargs["psa_ticket_id"] = "48291"

    session = AISession(**session_kwargs)
    test_db.add(session)
    await test_db.commit()
    await test_db.refresh(session)
    return session


# ── Resolve: local-only ────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_resolve_local_only_when_no_psa_ticket(
    client: AsyncClient, test_user, auth_headers, test_db
):
    session = await _make_session(test_db, test_user, with_psa=False)
    r = await client.post(
        f"/api/v1/ai-sessions/{session.id}/resolution-note/post",
        headers=auth_headers,
        json={"markdown": "## Problem\nx\n\n## Resolution\nfixed"},
    )
    assert r.status_code == 200
    body = r.json()
    assert body["outcome"] == "resolved_local"
    assert body["session_status"] == "resolved"
    assert body["external_id"] is None

    await test_db.refresh(session)
    assert session.status == "resolved"
    assert session.resolution_note_markdown == "## Problem\nx\n\n## Resolution\nfixed"
    assert session.resolved_at is not None


# ── Resolve: happy path (PSA post + status transition verified) ────────────

@pytest.mark.asyncio
async def test_resolve_posts_to_psa_and_verifies_status(
    client: AsyncClient, test_user, auth_headers, test_db
):
    session = await _make_session(test_db, test_user, with_psa=True)

    # Configure the Resolved status ID so the transition is attempted.
    await AccountSettings.set_setting(
        test_db, session.account_id, "cw_resolved_status_id", 42,
    )
    await test_db.commit()

    # Mock provider: post_note returns a fake note, update_ticket_status
    # returns anything, get_ticket returns the new status_id (matches 42
    # → verification passes).
    fake_provider = AsyncMock()
    fake_provider.post_note = AsyncMock(return_value=PSANote(
        id="cw-note-777", text="...", note_type=NoteType.RESOLUTION, created_at=None,
    ))
    fake_provider.update_ticket_status = AsyncMock(return_value=None)
    fake_provider.get_ticket = AsyncMock(return_value=PSATicket(
        id="48291", summary="t", status_id=42, status_name="Resolved",
    ))

    with patch(
        "app.services.psa_writeback_service.get_provider_for_connection",
        return_value=fake_provider,
    ):
        r = await client.post(
            f"/api/v1/ai-sessions/{session.id}/resolution-note/post",
            headers=auth_headers,
            json={"markdown": "## Problem\nx\n\n## Resolution\nfixed"},
        )

    assert r.status_code == 200
    body = r.json()
    assert body["outcome"] == "resolved"
    assert body["external_id"] == "cw-note-777"
    assert body["verified_status_id"] == 42
    assert body["verified_status_name"] == "Resolved"

    # post_note must have used the RESOLUTION note type
    fake_provider.post_note.assert_awaited_once()
    called_note_type = fake_provider.post_note.await_args.kwargs["note_type"]
    assert called_note_type == NoteType.RESOLUTION


# ── Resolve: status verification failure → 502 ──────────────────────────────

@pytest.mark.asyncio
async def test_resolve_surfaces_status_verification_failure(
    client: AsyncClient, test_user, auth_headers, test_db
):
    """CW silently rejecting a status change must NOT report silent success."""
    session = await _make_session(test_db, test_user, with_psa=True)
    await AccountSettings.set_setting(
        test_db, session.account_id, "cw_resolved_status_id", 42,
    )
    await test_db.commit()

    fake_provider = AsyncMock()
    fake_provider.post_note = AsyncMock(return_value=PSANote(
        id="cw-note-alpha", text="...", note_type=NoteType.RESOLUTION, created_at=None,
    ))
    fake_provider.update_ticket_status = AsyncMock(return_value=None)
    # get_ticket returns a DIFFERENT status_id — the transition didn't stick.
    fake_provider.get_ticket = AsyncMock(return_value=PSATicket(
        id="48291", summary="t", status_id=99, status_name="In Progress",
    ))

    with patch(
        "app.services.psa_writeback_service.get_provider_for_connection",
        return_value=fake_provider,
    ):
        r = await client.post(
            f"/api/v1/ai-sessions/{session.id}/resolution-note/post",
            headers=auth_headers,
            json={"markdown": "## Problem\nx\n\n## Resolution\nfixed"},
        )

    assert r.status_code == 502
    assert "did not verify" in r.json()["detail"]


# ── Resolve: skip status transition when not configured ────────────────────

@pytest.mark.asyncio
async def test_resolve_skips_status_transition_when_unconfigured(
    client: AsyncClient, test_user, auth_headers, test_db
):
    """No cw_resolved_status_id setting → post the note, don't touch status, not an error."""
    session = await _make_session(test_db, test_user, with_psa=True)
    # Deliberately no AccountSettings row.

    fake_provider = AsyncMock()
    fake_provider.post_note = AsyncMock(return_value=PSANote(
        id="cw-note-beta", text="...", note_type=NoteType.RESOLUTION, created_at=None,
    ))

    with patch(
        "app.services.psa_writeback_service.get_provider_for_connection",
        return_value=fake_provider,
    ):
        r = await client.post(
            f"/api/v1/ai-sessions/{session.id}/resolution-note/post",
            headers=auth_headers,
            json={"markdown": "## Problem\nx\n\n## Resolution\nfixed"},
        )

    assert r.status_code == 200
    body = r.json()
    assert body["outcome"] == "resolved"
    assert body["verified_status_id"] is None
    assert body["status_transition_skipped_reason"] is not None
    fake_provider.update_ticket_status.assert_not_called()
    fake_provider.get_ticket.assert_not_called()


# ── Resolve: already-resolved → 409 ─────────────────────────────────────────

@pytest.mark.asyncio
async def test_resolve_rejects_already_resolved_session(
    client: AsyncClient, test_user, auth_headers, test_db
):
    session = await _make_session(test_db, test_user, with_psa=False)
    session.status = "resolved"
    await test_db.commit()

    r = await client.post(
        f"/api/v1/ai-sessions/{session.id}/resolution-note/post",
        headers=auth_headers,
        json={"markdown": "..."},
    )
    assert r.status_code == 409


# ── Escalate: local-only + PSA parallels ────────────────────────────────────

@pytest.mark.asyncio
async def test_escalate_local_only_when_no_psa_ticket(
    client: AsyncClient, test_user, auth_headers, test_db
):
    session = await _make_session(test_db, test_user, with_psa=False)
    r = await client.post(
        f"/api/v1/ai-sessions/{session.id}/escalation-package/post",
        headers=auth_headers,
        json={"markdown": "## Problem\nx\n\n## Suggested next steps\n- try X"},
    )
    assert r.status_code == 200
    assert r.json()["outcome"] == "escalated_local"

    await test_db.refresh(session)
    assert session.status == "escalated"
    assert session.escalation_package_markdown is not None


@pytest.mark.asyncio
async def test_escalate_posts_internal_note_to_psa(
    client: AsyncClient, test_user, auth_headers, test_db
):
    """Escalation handoff posts as INTERNAL_ANALYSIS (not customer-visible)."""
    session = await _make_session(test_db, test_user, with_psa=True)

    fake_provider = AsyncMock()
    fake_provider.post_note = AsyncMock(return_value=PSANote(
        id="cw-note-esc", text="...", note_type=NoteType.INTERNAL_ANALYSIS, created_at=None,
    ))

    with patch(
        "app.services.psa_writeback_service.get_provider_for_connection",
        return_value=fake_provider,
    ):
        r = await client.post(
            f"/api/v1/ai-sessions/{session.id}/escalation-package/post",
            headers=auth_headers,
            json={"markdown": "## Problem\nx\n\n## Suggested next steps\n- try X"},
        )

    assert r.status_code == 200
    body = r.json()
    assert body["outcome"] == "escalated"
    assert body["external_id"] == "cw-note-esc"

    # Handoff packages are internal — must NOT be posted with RESOLUTION or DESCRIPTION flags.
    called = fake_provider.post_note.await_args.kwargs
    assert called["note_type"] == NoteType.INTERNAL_ANALYSIS
