"""Teste T2 — reconciliere anti-duplicat + retry/backoff + recuperare orfane.

Matcher pur (app.reconcile) + functiile worker-ului cu un RAR fake si DB temporara.
Testul-cheie: raspuns pierdut -> reconciliere gaseste recordul -> 'sent' fara re-POST.
"""

from __future__ import annotations

import json
import os
import tempfile

import httpx
import pytest

from app.rar_client import RarError
from app.reconcile import match_finalizata

# --- Matcher (unit) ---

_FIN = [
    {"id": 63622, "vin": "WBA1234567890", "dataPrestatie": "2024-02-05", "odometruFinal": 125000},
    {"id": 63625, "vin": "WBA1234567890", "dataPrestatie": "2024-02-05", "odometruFinal": 125000},
    {"id": 68514, "vin": "WVWZZZ1KZAW000123", "dataPrestatie": "2026-06-15", "odometruFinal": 123456},
]


def test_match_gaseste_pe_vin_data_odo():
    assert match_finalizata(_FIN, vin="WVWZZZ1KZAW000123", data_prestatie="2026-06-15", odometru_final="123456") == 68514


def test_match_vin_case_insensitive():
    assert match_finalizata(_FIN, vin="wvwzzz1kzaw000123", data_prestatie="2026-06-15", odometru_final="123456") == 68514


def test_match_odo_string_vs_int():
    # payload-ul nostru are odometruFinal string; RAR il intoarce int.
    assert match_finalizata(_FIN, vin="WBA1234567890", data_prestatie="2024-02-05", odometru_final="125000") == 63625


def test_match_duplicate_alege_id_maxim():
    # 63622 si 63625 sunt identice -> alege maximul.
    assert match_finalizata(_FIN, vin="WBA1234567890", data_prestatie="2024-02-05", odometru_final=125000) == 63625


def test_match_fara_potrivire():
    assert match_finalizata(_FIN, vin="WVWZZZ1KZAW000123", data_prestatie="2026-06-15", odometru_final="999") is None


def test_match_data_diferita():
    assert match_finalizata(_FIN, vin="WBA1234567890", data_prestatie="2025-01-01", odometru_final="125000") is None


# --- Worker (integration cu FakeRar + DB temporara) ---

class FakeRar:
    def __init__(self, *, finalizate=None, post_result=None, post_exc=None):
        self.finalizate = finalizate or []
        self.post_result = post_result if post_result is not None else {"id": 1000}
        self.post_exc = post_exc
        self.post_calls = 0
        self.finalizate_calls = 0

    def get_finalizate(self, token):
        self.finalizate_calls += 1
        return self.finalizate

    def post_prezentare(self, token, payload):
        self.post_calls += 1
        if self.post_exc is not None:
            raise self.post_exc
        return self.post_result


@pytest.fixture()
def env(monkeypatch):
    tmp = tempfile.mkdtemp()
    monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
    from app.config import get_settings
    get_settings.cache_clear()
    from app.db import get_connection, init_db
    init_db()
    conn = get_connection()
    settings = get_settings()
    yield conn, settings
    conn.close()
    get_settings.cache_clear()


_CONTENT = {
    "vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST",
    "data_prestatie": "2026-06-15", "odometru_final": "123456",
    "prestatii": [{"cod_prestatie": "OE-1"}], "sistem_reparat": "null",
}


def _insert(conn, status="queued", content=None, sending_since=None, retry_count=0):
    content = content or _CONTENT
    cur = conn.execute(
        "INSERT INTO submissions (idempotency_key, status, payload_json, sending_since, retry_count) "
        "VALUES (?, ?, ?, ?, ?)",
        (f"key-{os.urandom(4).hex()}", status, json.dumps(content), sending_since, retry_count),
    )
    return int(cur.lastrowid)


def _status(conn, sid):
    return conn.execute("SELECT * FROM submissions WHERE id=?", (sid,)).fetchone()


def test_happy_path_sent(env):
    from app.worker.__main__ import process_one
    conn, settings = env
    sid = _insert(conn)
    rar = FakeRar(post_result={"id": 555})
    out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
    assert out == "sent"
    row = _status(conn, sid)
    assert row["status"] == "sent" and row["id_prezentare"] == 555
    assert rar.post_calls == 1


def test_raspuns_pierdut_reconciliere_fara_duplicat(env):
    """POST esueaza tranzitoriu DAR RAR a inregistrat -> reconciliere -> sent, fara re-POST."""
    from app.worker.__main__ import process_one
    conn, settings = env
    sid = _insert(conn)
    # 503 = tranzitoriu; recordul EXISTA deja la RAR (raspunsul s-a pierdut).
    rar = FakeRar(
        finalizate=[{"id": 68514, "vin": "WVWZZZ1KZAW000123", "dataPrestatie": "2026-06-15", "odometruFinal": 123456}],
        post_exc=RarError("502 bad gateway", status_code=503),
    )
    out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
    assert out == "sent"
    row = _status(conn, sid)
    assert row["status"] == "sent" and row["id_prezentare"] == 68514
    assert rar.post_calls == 1          # NU s-a re-trimis
    assert rar.finalizate_calls == 1    # a reconciliat


def test_rar_500_definitiv_devine_error_fara_reconciliere(env):
    """RAR 500 cu mesaj (ex. ORA-12899) = esec DEFINITIV: NU reconcilia, NU reincerca.

    Bug-ul real: 500 era tratat ca tranzitoriu -> reconciliere -> marca fals 'sent'
    pe un record PARTIAL creat de RAR (ne-tranzactional). Aici, chiar daca exista un
    record care s-ar potrivi, NU trebuie marcat sent.
    """
    from app.worker.__main__ import process_one
    conn, settings = env
    sid = _insert(conn)
    rar = FakeRar(
        finalizate=[{"id": 99999, "vin": "WVWZZZ1KZAW000123",
                     "dataPrestatie": "2026-06-15", "odometruFinal": 123456}],
        post_exc=RarError(
            "postPrezentare esuat (HTTP 500)", status_code=500,
            rar_message="Eroare la adaugarea prezentarii : ORA-12899: value too large for column COD_PRESTATIE",
        ),
    )
    out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
    assert out == "error"
    row = _status(conn, sid)
    assert row["status"] == "error"
    assert row["id_prezentare"] is None          # NU fals sent
    assert rar.post_calls == 1                    # nu re-trimite
    assert rar.finalizate_calls == 0              # NU reconciliaza
    err = json.loads(row["rar_error"])
    assert err["cod"] == "RAR_EROARE_SERVER" and "ORA-12899" in err["cauza"]


def test_rar_503_ramane_tranzitoriu(env):
    """Boundary: doar 500-cu-mesaj e permanent; 503 (infra) ramane ambiguu -> reconciliere."""
    from app.worker.__main__ import process_one
    conn, settings = env
    sid = _insert(conn)
    rar = FakeRar(
        finalizate=[{"id": 68514, "vin": "WVWZZZ1KZAW000123",
                     "dataPrestatie": "2026-06-15", "odometruFinal": 123456}],
        post_exc=RarError("service unavailable", status_code=503, rar_message="temporar indisponibil"),
    )
    out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
    assert out == "sent"
    assert _status(conn, sid)["id_prezentare"] == 68514
    assert rar.finalizate_calls == 1


def test_tranzitoriu_neinregistrat_requeue(env):
    from app.worker.__main__ import process_one
    conn, settings = env
    sid = _insert(conn)
    rar = FakeRar(finalizate=[], post_exc=httpx.ConnectError("conn refused"))
    out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
    assert out == "requeued"
    row = _status(conn, sid)
    assert row["status"] == "queued"
    assert row["retry_count"] == 1
    assert row["next_attempt_at"] is not None


def test_validare_400_needs_data(env):
    from app.worker.__main__ import process_one
    conn, settings = env
    sid = _insert(conn)
    rar = FakeRar(post_exc=RarError("Validare esuata", status_code=400,
                                    field_errors=[{"field": "vin", "message": "rau"}]))
    out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
    assert out == "needs_data"
    assert _status(conn, sid)["status"] == "needs_data"
    assert rar.post_calls == 1


def test_4xx_nerecuperabil_error(env):
    from app.worker.__main__ import process_one
    conn, settings = env
    sid = _insert(conn)
    rar = FakeRar(post_exc=RarError("forbidden", status_code=403))
    out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
    assert out == "error"
    assert _status(conn, sid)["status"] == "error"


def test_retry_peste_maxim_devine_error(env):
    from app.worker.__main__ import requeue_with_backoff
    conn, settings = env
    sid = _insert(conn, retry_count=settings.worker_max_retries)  # urmatorul requeue depaseste maximul
    requeue_with_backoff(conn, settings, sid, reason="tranzitoriu persistent")
    assert _status(conn, sid)["status"] == "error"


def test_recover_orphan_reconciliat(env):
    from app.worker.__main__ import recover_orphans
    conn, settings = env
    # rand 'sending' vechi (lease depasit) -> orfan; recordul exista la RAR.
    sid = _insert(conn, status="sending", sending_since="2000-01-01T00:00:00+00:00")
    rar = FakeRar(finalizate=[{"id": 77777, "vin": "WVWZZZ1KZAW000123",
                               "dataPrestatie": "2026-06-15", "odometruFinal": 123456}])
    n = recover_orphans(conn, settings, rar, "tok")
    assert n == 1
    row = _status(conn, sid)
    assert row["status"] == "sent" and row["id_prezentare"] == 77777


def test_recover_orphan_neinregistrat_requeue(env):
    from app.worker.__main__ import recover_orphans
    conn, settings = env
    sid = _insert(conn, status="sending", sending_since="2000-01-01T00:00:00+00:00")
    rar = FakeRar(finalizate=[])
    recover_orphans(conn, settings, rar, "tok")
    assert _status(conn, sid)["status"] == "queued"


def test_recover_orphan_ignora_randul_proaspat_claim(env):
    """Regresie format data: un rand PROASPAT revendicat NU trebuie tratat ca orfan.

    claim_one scrie sending_since cu datetime('now') -> 'YYYY-MM-DD HH:MM:SS' (spatiu).
    Bug-ul: cutoff calculat cu _iso() era in format ISO ('T'), iar la comparatie de string
    spatiul (0x20) < 'T' (0x54) facea ca ORICE rand 'sending' sa para <= cutoff -> lease-ul
    de 120s era ignorat si fiecare rand revendicat era reconciliat/requeue-uit instant.
    """
    from app.worker.__main__ import claim_one, recover_orphans
    conn, settings = env
    _insert(conn)                       # queued
    claimed = claim_one(conn)           # -> sending, sending_since=datetime('now')
    assert claimed is not None
    # Chiar daca recordul ar exista la RAR, randul proaspat nu trebuie atins.
    rar = FakeRar(finalizate=[{"id": 999, "vin": "WVWZZZ1KZAW000123",
                               "dataPrestatie": "2026-06-15", "odometruFinal": 123456}])
    n = recover_orphans(conn, settings, rar, "tok")
    assert n == 0                       # nu l-a recuperat
    assert rar.finalizate_calls == 0    # nici macar nu a interogat RAR
    assert _status(conn, claimed["id"])["status"] == "sending"  # neatins


def test_recover_orphan_lease_depasit_format_sqlite(env):
    """Complementar: un rand 'sending' mai vechi decat lease-ul (format SQLite, spatiu) ESTE orfan."""
    from app.worker.__main__ import recover_orphans
    conn, settings = env
    cur = conn.execute(
        "INSERT INTO submissions (idempotency_key, status, payload_json, sending_since) "
        "VALUES (?, 'sending', ?, datetime('now', '-1 hour'))",
        (f"key-{os.urandom(4).hex()}", json.dumps(_CONTENT)),
    )
    sid = int(cur.lastrowid)
    rar = FakeRar(finalizate=[{"id": 888, "vin": "WVWZZZ1KZAW000123",
                               "dataPrestatie": "2026-06-15", "odometruFinal": 123456}])
    n = recover_orphans(conn, settings, rar, "tok")
    assert n == 1
    assert _status(conn, sid)["status"] == "sent"


def test_claim_respecta_next_attempt_at(env):
    from app.worker.__main__ import claim_one
    conn, _ = env
    # next_attempt_at in viitor -> nu se ia.
    conn.execute(
        "INSERT INTO submissions (idempotency_key, status, payload_json, next_attempt_at) "
        "VALUES ('k1','queued',?, '2999-01-01T00:00:00+00:00')",
        (json.dumps(_CONTENT),),
    )
    assert claim_one(conn) is None
    # next_attempt_at in trecut -> se ia.
    conn.execute(
        "INSERT INTO submissions (idempotency_key, status, payload_json, next_attempt_at) "
        "VALUES ('k2','queued',?, '2000-01-01T00:00:00+00:00')",
        (json.dumps(_CONTENT),),
    )
    claimed = claim_one(conn)
    assert claimed is not None
