from datetime import date
from decimal import Decimal

import pytest

from cyclone.parsers.models import (
    Address,
    BillingProvider,
    ClaimHeader,
    ClaimOutput,
    Diagnosis,
    Envelope,
    Payer,
    Procedure,
    ServiceLine,
    Subscriber,
    ValidationReport,
)
from cyclone.parsers.payer import PayerConfig
from cyclone.parsers.validator import validate


def _build_claim(**overrides) -> ClaimOutput:
    """Build a passing claim for tests; override fields to break specific rules."""
    base = dict(
        claim_id="C1",
        control_number="991102977",
        transaction_date=date(2026, 6, 11),
        billing_provider=BillingProvider(
            name="Test Provider",
            npi="1234567890",
            tax_id="123456789",
            address=Address(line1="1 Main", city="X", state="CO", zip="80000"),
        ),
        subscriber=Subscriber(
            first_name="John",
            last_name="Doe",
            member_id="M1",
            dob=date(1980, 1, 1),
            gender="M",
            address=Address(line1="1 Main", city="X", state="CO", zip="80000"),
        ),
        payer=Payer(name="COHCPF", id="SKCO0"),
        claim=ClaimHeader(
            claim_id="C1",
            total_charge=Decimal("100.00"),
            place_of_service="12",
            frequency_code="1",
            provider_signature="Y",
            assignment="Y",
            release_of_info="Y",
        ),
        diagnoses=[Diagnosis(code="Z00", qualifier="ABK")],
        service_lines=[
            ServiceLine(
                line_number=1,
                procedure=Procedure(qualifier="HC", code="99213", modifiers=[]),
                charge=Decimal("100.00"),
                unit_type="UN",
                units=Decimal("1.0"),
                service_date=date(2026, 6, 11),
            )
        ],
        validation=ValidationReport(passed=True, errors=[], warnings=[]),
        raw_segments=[],
    )
    base.update(overrides)
    return ClaimOutput(**base)


def test_validate_passing_claim():
    cfg = PayerConfig.co_medicaid()
    report = validate(_build_claim(), cfg)
    assert report.passed is True
    assert report.errors == []


def test_r010_clm01_required():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.claim.claim_id = ""
    report = validate(claim, cfg)
    assert not report.passed
    assert any(i.rule == "R010_clm01_present" for i in report.errors)


def test_r011_total_charge_positive():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.claim.total_charge = Decimal("0.00")
    report = validate(claim, cfg)
    assert any(i.rule == "R011_total_charge_positive" for i in report.errors)


def test_r020_npi_must_be_ten_digits():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.billing_provider.npi = "12345"
    report = validate(claim, cfg)
    assert any(i.rule == "R020_npi_format" for i in report.errors)


# --------------------------------------------------------------------------- #
# R021 — NPI Luhn checksum (SP20)
# --------------------------------------------------------------------------- #


def test_r021_npi_checksum_valid_passes_silently():
    """A valid Luhn NPI (CMS-published 1234567893) yields no R021 issue."""
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.billing_provider.npi = "1234567893"
    report = validate(claim, cfg)
    assert not any(i.rule == "R021_npi_checksum" for i in report.errors + report.warnings)


def test_r021_npi_checksum_bad_luhn_is_warning():
    """An NPI that passes format but fails Luhn is a WARNING, not an error."""
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.billing_provider.npi = "1234567890"  # right length, wrong check digit
    report = validate(claim, cfg)
    assert report.passed is True  # WARNINGs don't fail the report
    assert any(i.rule == "R021_npi_checksum" and i.severity == "warning" for i in report.warnings)


def test_r021_npi_checksum_skipped_when_format_bad():
    """When R020 already flagged the format, R021 stays silent
    (avoids a duplicate 'this NPI is wrong' message)."""
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.billing_provider.npi = "12345"  # wrong length
    report = validate(claim, cfg)
    # R020 errors as expected; R021 stays quiet.
    assert any(i.rule == "R020_npi_format" for i in report.errors)
    assert not any(i.rule == "R021_npi_checksum" for i in report.errors + report.warnings)


def test_r021_npi_checksum_skipped_when_npi_missing():
    """An empty NPI doesn't trigger R021 (only R020 would, but R020
    only fires when NPI is *present* and wrong). R021 must stay silent
    when NPI is empty so we don't double-fire."""
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.billing_provider.npi = ""
    report = validate(claim, cfg)
    assert not any(i.rule == "R021_npi_checksum" for i in report.errors + report.warnings)


def test_r030_frequency_allowed():
    cfg = PayerConfig.co_medicaid()  # only 1, 7, 8
    claim = _build_claim()
    claim.claim.frequency_code = "5"
    report = validate(claim, cfg)
    assert any(i.rule == "R030_frequency_allowed" for i in report.errors)


def test_r031_ref_g1_optional_no_error():
    """R031 is informational in v1 — no REF*G1 should not error."""
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    report = validate(claim, cfg)
    assert not any(i.rule == "R031_ref_g1_optional" and i.severity == "error" for i in report.errors)


def test_r050_diagnosis_required():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim(diagnoses=[])
    report = validate(claim, cfg)
    assert any(i.rule == "R050_diagnosis_present" for i in report.errors)


def test_r060_service_dates_required():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.service_lines[0].service_date = None
    report = validate(claim, cfg)
    assert any(i.rule == "R060_service_dates_present" for i in report.errors)
    # Now add the date:
    claim.service_lines[0].service_date = date(2026, 6, 11)
    report = validate(claim, cfg)
    assert not any(i.rule == "R060_service_dates_present" for i in report.errors + report.warnings)


def test_r070_charges_sum_warning():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.claim.total_charge = Decimal("999.00")  # mismatch
    report = validate(claim, cfg)
    assert any(i.rule == "R070_charges_sum" and i.severity == "warning" for i in report.warnings)


def test_r100_payer_id_warning_only():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.payer.id = "WRONG"
    report = validate(claim, cfg)
    assert any(i.rule == "R100_payer_id_matches" and i.severity == "warning" for i in report.warnings)
    assert report.passed is True


def test_r032_passes_when_qualifier_b():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.claim.facility_code_qualifier = "B"
    report = validate(claim, cfg)
    assert not any(i.rule == "R032_clm05_2_facility_qualifier" for i in report.errors + report.warnings)


def test_r032_errors_when_qualifier_unknown():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.claim.facility_code_qualifier = "X"
    report = validate(claim, cfg)
    assert any(i.rule == "R032_clm05_2_facility_qualifier" and i.severity == "error" for i in report.errors)
    assert report.passed is False


def test_r032_skipped_when_qualifier_none():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    # Default _build_claim leaves facility_code_qualifier unset (None).
    assert claim.claim.facility_code_qualifier is None
    report = validate(claim, cfg)
    assert not any(i.rule == "R032_clm05_2_facility_qualifier" for i in report.errors + report.warnings)


def test_r033_passes_with_valid_pos_code():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.claim.place_of_service = "12"  # Office — valid CMS POS
    report = validate(claim, cfg)
    assert not any(i.rule == "R033_clm05_1_place_of_service_code" for i in report.errors + report.warnings)


def test_r033_errors_with_invalid_pos_code():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim()
    claim.claim.place_of_service = "999"  # not a valid CMS POS code
    report = validate(claim, cfg)
    assert any(i.rule == "R033_clm05_1_place_of_service_code" and i.severity == "error" for i in report.errors)
    assert report.passed is False


# --------------------------------------------------------------------------- #
# R034 — REF*G1 enforcement (SP3 Phase 1)
# --------------------------------------------------------------------------- #


def _strict_cfg() -> PayerConfig:
    """Strict cfg that mirrors the CO Medicaid defaults but turns on R034 enforcement.

    We don't modify :meth:`PayerConfig.co_medicaid` — the lenient default stays
    in v1. This local fixture lets us exercise the strict path without leaking
    config changes across the suite.
    """
    return PayerConfig(
        name="StrictTest",
        require_ref_g1_for_adjustments=True,
        allowed_bht06={"CH"},
        payer_id="X",
    )


def test_r034_ref_g1_required_freq_7_no_ref_g1_errors():
    cfg = _strict_cfg()
    claim = _build_claim()
    claim.claim.frequency_code = "7"
    claim.raw_segments = []
    report = validate(claim, cfg)
    assert any(
        i.rule == "R034_ref_g1_required" and i.severity == "error" for i in report.errors
    )
    assert report.passed is False


def test_r034_ref_g1_required_freq_8_no_ref_g1_errors():
    cfg = _strict_cfg()
    claim = _build_claim()
    claim.claim.frequency_code = "8"
    claim.raw_segments = []
    report = validate(claim, cfg)
    assert any(
        i.rule == "R034_ref_g1_required" and i.severity == "error" for i in report.errors
    )
    assert report.passed is False


def test_r034_ref_g1_required_freq_7_with_ref_g1_passes():
    cfg = _strict_cfg()
    claim = _build_claim()
    claim.claim.frequency_code = "7"
    claim.raw_segments = [["REF", "G1", "12345"]]
    report = validate(claim, cfg)
    assert not any(
        i.rule == "R034_ref_g1_required" for i in report.errors + report.warnings
    )


def test_r034_ref_g1_required_freq_1_no_ref_g1_passes():
    cfg = _strict_cfg()
    claim = _build_claim()
    claim.claim.frequency_code = "1"
    claim.raw_segments = []
    report = validate(claim, cfg)
    assert not any(
        i.rule == "R034_ref_g1_required" for i in report.errors + report.warnings
    )


def test_r034_ref_g1_lenient_cfg_never_errors():
    cfg = PayerConfig.co_medicaid()  # require_ref_g1_for_adjustments=False (lenient v1)
    claim = _build_claim()
    claim.claim.frequency_code = "7"
    claim.raw_segments = []
    report = validate(claim, cfg)
    assert not any(
        i.rule == "R034_ref_g1_required" for i in report.errors + report.warnings
    )


# --------------------------------------------------------------------------- #
# R035 — BHT06 transaction type code (SP3 Phase 1)
# --------------------------------------------------------------------------- #


def test_r035_bht06_allowed_ch_passes():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim(transaction_type_code="CH")
    report = validate(claim, cfg)
    assert not any(
        i.rule == "R035_bht06_allowed" for i in report.errors + report.warnings
    )


def test_r035_bht06_allowed_rp_errors_for_co_medicaid():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim(transaction_type_code="RP")
    report = validate(claim, cfg)
    assert any(
        i.rule == "R035_bht06_allowed" and i.severity == "error" for i in report.errors
    )
    assert report.passed is False


def test_r035_bht06_missing_skips():
    cfg = PayerConfig.co_medicaid()
    claim = _build_claim(transaction_type_code=None)
    assert claim.transaction_type_code is None
    report = validate(claim, cfg)
    assert not any(
        i.rule == "R035_bht06_allowed" for i in report.errors + report.warnings
    )


def test_r035_bht06_rp_allowed_for_generic_837p():
    cfg = PayerConfig.generic_837p()  # allows {"CH", "RP"}
    claim = _build_claim(transaction_type_code="RP")
    report = validate(claim, cfg)
    assert not any(
        i.rule == "R035_bht06_allowed" for i in report.errors + report.warnings
    )
