"""SP39: tests for the `cyclone resubmissions status` CLI."""
from __future__ import annotations

from datetime import datetime, timezone

from click.testing import CliRunner

from cyclone import db as cycl_db
from cyclone.cli import resubmissions_status_cmd
from cyclone.db import Resubmission


def _seed_one(claim_id: str = "claim-1", batch_id: str = "batch-1",
              icn: str = "000000001") -> None:
    cycl_db.init_db()
    with cycl_db.SessionLocal()() as s:
        s.add(Resubmission(
            claim_id=claim_id,
            batch_id=batch_id,
            resubmitted_at=datetime.now(timezone.utc),
            source_corrected_path=f"/tmp/{claim_id}.x12",
            interchange_control_number=icn,
            group_control_number="1",
        ))
        s.commit()


def _cleanup(batch_id: str) -> None:
    with cycl_db.SessionLocal()() as s:
        s.query(Resubmission).filter(Resubmission.batch_id == batch_id).delete()
        s.commit()


def test_no_resubmissions_prints_message_and_exits_0():
    cycl_db.init_db()
    runner = CliRunner()
    result = runner.invoke(
        resubmissions_status_cmd,
        ["--batch-id", "no-such-batch-zzz"],
    )
    assert result.exit_code == 0, result.output
    assert "no resubmissions recorded" in result.output


def test_one_resubmission_prints_status_row():
    try:
        _seed_one(claim_id="claim-A", batch_id="batch-A")
        runner = CliRunner()
        result = runner.invoke(
            resubmissions_status_cmd,
            ["--batch-id", "batch-A"],
        )
        assert result.exit_code == 0, result.output
        assert "claim-A" in result.output
        assert "pending_999" in result.output  # no inbound ack yet
        assert "batch-A" in result.output
    finally:
        _cleanup("batch-A")


def test_limit_truncates_output():
    try:
        for i in range(5):
            _seed_one(
                claim_id=f"claim-L{i}", batch_id="batch-L",
                icn=f"00000000{i}",
            )
        runner = CliRunner()
        result = runner.invoke(
            resubmissions_status_cmd,
            ["--batch-id", "batch-L", "--limit", "2"],
        )
        assert result.exit_code == 0, result.output
        assert "more" in result.output
        # Only 2 rows printed in the table (plus 1 "(N more)" line).
        body_lines = [
            ln for ln in result.output.splitlines()
            if ln.startswith("claim-L")
        ]
        assert len(body_lines) == 2
    finally:
        _cleanup("batch-L")


def test_status_reflects_paid_when_remittance_links():
    """When a remittance.claim_id links to the resubmitted claim,
    the payment column shows 'paid'."""
    try:
        from cyclone.db import Remittance
        from decimal import Decimal
        _seed_one(claim_id="claim-P", batch_id="batch-P")
        with cycl_db.SessionLocal()() as s:
            s.add(Remittance(
                id="remit-P-1",
                batch_id="batch-P",
                payer_claim_control_number="claim-P",
                claim_id="claim-P",
                status_code="1",
                status_label="Processed as Primary",
                total_charge=Decimal("100.00"),
                total_paid=Decimal("80.00"),
                adjustment_amount=Decimal("0"),
                claim_level_adjustment_amount=Decimal("0"),
                received_at=datetime.now(timezone.utc),
            ))
            s.commit()
        runner = CliRunner()
        result = runner.invoke(
            resubmissions_status_cmd,
            ["--batch-id", "batch-P"],
        )
        assert result.exit_code == 0, result.output
        assert "claim-P" in result.output
        assert "paid" in result.output
    finally:
        from cyclone.db import Remittance as _R
        with cycl_db.SessionLocal()() as s:
            s.query(_R).filter(_R.id == "remit-P-1").delete()
            s.commit()
        _cleanup("batch-P")

# ---------------------------------------------------------------------------
# SP39: `cyclone resubmit-rejected-claims` instruments Resubmission rows.
# ---------------------------------------------------------------------------


def _seed_clearhouse_stub_false(tmp_path) -> None:
    """Seed the clearhouse singleton and flip ``sftp_block.stub=False``
    with a non-routable host so the CLI proceeds past the guard and
    the paramiko stub below intercepts the connection."""
    import json
    from cyclone import db as db_mod
    from cyclone.db import ClearhouseORM
    from cyclone.store import store as cycl_store
    cycl_store.ensure_clearhouse_seeded()
    with db_mod.SessionLocal()() as s:
        ch = s.get(ClearhouseORM, 1)
        assert ch is not None, "ensure_clearhouse_seeded should have created it"
        block = json.loads(json.dumps(ch.sftp_block_json))
        block["stub"] = False
        block["host"] = "stub.invalid"
        ch.sftp_block_json = block
        s.commit()


def test_resubmit_rejected_claims_inserts_resubmission_row(tmp_path, monkeypatch):
    """SP39: after a successful upload, one Resubmission row per claim
    is inserted with the parent dir's batch_id and the parsed envelope
    control numbers."""
    import sys
    import types
    from cyclone.cli import resubmit_rejected_claims
    from cyclone.parsers.parse_837 import parse as parse_837_text
    from cyclone.parsers.payer import PayerConfig
    from cyclone.store import store as cycl_store

    _seed_clearhouse_stub_false(tmp_path)

    # Build a parseable 837P file (CO_TXIX payer, single claim).
    cfg = PayerConfig.co_medicaid()
    sample = tmp_path / "batch-deadbeef-1-claims"
    sample.mkdir()
    file_path = sample / "tp1-837P-test-1of1.x12"
    src_text = open("tests/fixtures/co_medicaid_837p.txt").read()
    file_path.write_text(src_text)

    parsed = parse_837_text(src_text, cfg)
    assert parsed.claims, "fixture must produce at least one claim"
    first_claim_id = parsed.claims[0].claim_id

    # Replace paramiko with a fake whose stat() raises (file not on
    # remote yet, so the CLI takes the upload branch).
    fake_paramiko = types.ModuleType("paramiko")

    class _FakeSFTP:
        def stat(self, path):
            raise IOError("not found")
        def put(self, local, remote):
            return None

    class _FakeSSH:
        def open_sftp(self):
            return _FakeSFTP()
        def set_missing_host_key_policy(self, policy):
            return None
        def connect(self, *a, **kw):
            return None
        def close(self):
            return None

    fake_paramiko.SSHClient = lambda: _FakeSSH()
    fake_paramiko.AutoAddPolicy = lambda: None
    monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)

    captured: list[dict] = []
    monkeypatch.setattr(
        cycl_store, "record_resubmission",
        lambda **kw: captured.append(kw) or True,
    )

    runner = CliRunner()
    result = runner.invoke(resubmit_rejected_claims, [
        "--ingest-dir", str(tmp_path),
        "--limit", "1",
        "--no-validate",
        "--skip-edifabric-validate",
    ])
    assert result.exit_code == 0, (
        f"CLI exited {result.exit_code}, output={result.output!r}, "
        f"exception={result.exception!r}"
    )
    assert len(captured) >= 1, (
        f"expected record_resubmission to be called, got {captured!r}"
    )
    call = captured[0]
    assert call["claim_id"] == first_claim_id, call
    assert call["batch_id"] == "deadbeef", call
    assert "deadbeef-1-claims" in call["source_corrected_path"], call
    assert call["interchange_control_number"], call
    assert call["group_control_number"], call


def test_resubmit_idempotency_does_not_create_duplicate_rows(tmp_path, monkeypatch):
    """A second run against the same file (now on the SFTP) does NOT
    insert a second Resubmission row — only the original upload branch
    writes, matching the audit-log behavior."""
    from cyclone.cli import resubmit_rejected_claims
    from cyclone.store import store as cycl_store
    import sys
    import types

    _seed_clearhouse_stub_false(tmp_path)

    # Set up: one corrected file.
    sample = tmp_path / "batch-cafebabe-1-claims"
    sample.mkdir()
    file_path = sample / "tp1-837P-test-1of1.x12"
    src_text = open("tests/fixtures/co_medicaid_837p.txt").read()
    file_path.write_text(src_text)
    on_remote_size = len(src_text.encode())

    # Build a paramiko fake where stat() returns matching size so the
    # CLI takes the skip branch. The CLI only reads ``st_size`` off the
    # return value, so a simple duck-typed object suffices.
    fake_paramiko = types.ModuleType("paramiko")

    class _FakeAttr:
        def __init__(self, size: int) -> None:
            self.st_size = size

    class _FakeSFTPAlready:
        def stat(self, path):
            return _FakeAttr(on_remote_size)

        def put(self, local, remote):
            raise AssertionError("should not put when file already on remote")

    class _FakeSSHAlready:
        def open_sftp(self):
            return _FakeSFTPAlready()
        def set_missing_host_key_policy(self, policy):
            return None
        def connect(self, *a, **kw):
            return None
        def close(self):
            return None

    fake_paramiko.SSHClient = lambda: _FakeSSHAlready()
    fake_paramiko.AutoAddPolicy = lambda: None
    monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)

    captured: list[dict] = []
    monkeypatch.setattr(
        cycl_store, "record_resubmission",
        lambda **kw: captured.append(kw) or True,
    )

    runner = CliRunner()
    result = runner.invoke(resubmit_rejected_claims, [
        "--ingest-dir", str(tmp_path),
        "--limit", "1",
        "--no-validate",
        "--skip-edifabric-validate",
    ])
    # On the skip path, record_resubmission should NOT be called.
    assert captured == [], f"expected no record_resubmission on skip, got {captured!r}"
    assert "skipped" in result.output, result.output


# ---------------------------------------------------------------------------
# SP40: `cyclone resubmit-rejected-claims` Edifabric pre-upload gate.
# ---------------------------------------------------------------------------


_TEST_KEY = "test-edifabric-key-0123456789abcdef"


def _install_edifabric_mock(handler):
    """Install a mocked Edifabric transport + get_secret stub for the
    duration of a single test. Returned teardown callable can be wired
    to monkeypatch via the autouse fixture pattern below."""
    import sys
    import httpx
    from cyclone import edifabric
    import cyclone.secrets as _secrets_mod

    real_get_secret = _secrets_mod.get_secret

    def _fake_get_secret(name):
        if name == "edifabric.api_key":
            return _TEST_KEY
        return real_get_secret(name)

    _secrets_mod.get_secret = _fake_get_secret
    transport = httpx.MockTransport(handler)
    edifabric.set_transport_factory(
        lambda: httpx.Client(transport=transport, timeout=10.0)
    )

    def _teardown():
        _secrets_mod.get_secret = real_get_secret
        edifabric._reset_transport_factory()

    return _teardown


def _two_files_setup(tmp_path):
    """Stage two .x12 files in distinct batch dirs so the test can
    observe one file's gate failure not blocking the other's upload."""
    src_text = open("tests/fixtures/co_medicaid_837p.txt").read()
    a = tmp_path / "batch-aaaaaaaa-1-claims"
    a.mkdir()
    (a / "tp1-aaaaaaaa-1of1.x12").write_text(src_text)
    b = tmp_path / "batch-bbbbbbbb-1-claims"
    b.mkdir()
    (b / "tp1-bbbbbbbb-1of1.x12").write_text(src_text)
    return src_text


def test_resubmit_edifabric_gate_aborts_bad_file_continues_batch(
    tmp_path, monkeypatch,
):
    """SP40: when Edifabric returns Status='error' for one file, that
    file is recorded as ``validate_failed`` (not uploaded, no
    Resubmission row), but the batch continues with the next file."""
    import sys
    import types
    from cyclone.cli import resubmit_rejected_claims

    _seed_clearhouse_stub_false(tmp_path)
    _two_files_setup(tmp_path)

    seen: list[str] = []
    x12 = {
        "SegmentDelimiter": "~",
        "DataElementDelimiter": "*",
        "ISA": {"InterchangeControlNumber_13": "000000001"},
        "Groups": [],
        "IEATrailers": [],
    }

    def edifabric_handler(request: httpx.Request) -> httpx.Response:
        if request.url.path.endswith("/read"):
            return httpx.Response(200, json=[x12])
        if request.url.path.endswith("/validate"):
            # Fail the FIRST validate call; succeed on the second.
            seen.append("validate")
            is_bad = len(seen) == 1
            return httpx.Response(200, json={
                "Status": "error" if is_bad else "success",
                "Details": (
                    [
                        {"SegmentId": "PER",
                         "Message": "PER-04 is required",
                         "Status": "error"},
                    ]
                    if is_bad else []
                ),
                "LastIndex": 5 if is_bad else 46,
            })
        raise AssertionError(f"unexpected path: {request.url.path}")

    import httpx
    teardown = _install_edifabric_mock(edifabric_handler)
    try:
        # Fake paramiko so SFTP doesn't actually fire.
        fake_paramiko = types.ModuleType("paramiko")

        class _FakeAttr:
            def __init__(self, size: int) -> None:
                self.st_size = size

        class _FakeSFTP:
            def __init__(self):
                self.put_calls: list[str] = []

            def stat(self, path):
                raise IOError("not found")

            def put(self, local, remote):
                self.put_calls.append(remote)

        fake_sftp = _FakeSFTP()

        class _FakeSSH:
            def open_sftp(self):
                return fake_sftp

            def set_missing_host_key_policy(self, policy):
                return None

            def connect(self, *a, **kw):
                return None

            def close(self):
                return None

        fake_paramiko.SSHClient = lambda: _FakeSSH()
        fake_paramiko.AutoAddPolicy = lambda: None
        monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)

        runner = CliRunner()
        result = runner.invoke(resubmit_rejected_claims, [
            "--ingest-dir", str(tmp_path),
            "--no-validate",
            # default: gate ENABLED
        ])
        assert result.exit_code == 0, result.output

        # First file: Edifabric gate aborted it (no SFTP put for it).
        # Second file: passed the gate, got uploaded via the SFTP put.
        assert len(fake_sftp.put_calls) == 1, fake_sftp.put_calls
        assert "bbbbbbbb" in fake_sftp.put_calls[0], fake_sftp.put_calls[0]

        # Output surfaces the failure list.
        assert "EDIFABRIC VALIDATION FAIL" in result.output, result.output
        assert "1 file(s) blocked by Edifabric gate" in result.output, result.output
        assert "aaaaaaa" in result.output, result.output
        assert "PER-04 is required" in result.output, result.output

        # DONE summary shows both counters.
        assert "validate_failed=1" in result.output, result.output
        assert "uploaded=1" in result.output, result.output
    finally:
        teardown()


def test_resubmit_edifabric_skip_flag_bypasses_gate(tmp_path, monkeypatch):
    """SP40: ``--skip-edifabric-validate`` bypasses the gate entirely
    so the same Edifabric-failing file uploads via SFTP. Used for
    dev / hot-fix scenarios."""
    import sys
    import types
    from cyclone.cli import resubmit_rejected_claims

    _seed_clearhouse_stub_false(tmp_path)
    _two_files_setup(tmp_path)

    import httpx
    x12 = {
        "SegmentDelimiter": "~",
        "DataElementDelimiter": "*",
        "ISA": {"InterchangeControlNumber_13": "000000001"},
        "Groups": [],
        "IEATrailers": [],
    }

    validate_calls: list[str] = []

    def edifabric_handler(request: httpx.Request) -> httpx.Response:
        if request.url.path.endswith("/read"):
            return httpx.Response(200, json=[x12])
        if request.url.path.endswith("/validate"):
            validate_calls.append("hit")
            return httpx.Response(200, json={
                "Status": "error",
                "Details": [{"SegmentId": "PER",
                             "Message": "PER-04 is required",
                             "Status": "error"}],
                "LastIndex": 5,
            })
        raise AssertionError(f"unexpected path: {request.url.path}")

    teardown = _install_edifabric_mock(edifabric_handler)
    try:
        fake_paramiko = types.ModuleType("paramiko")

        class _FakeSFTP:
            def __init__(self):
                self.put_calls: list[str] = []

            def stat(self, path):
                raise IOError("not found")

            def put(self, local, remote):
                self.put_calls.append(remote)

        fake_sftp = _FakeSFTP()

        class _FakeSSH:
            def open_sftp(self):
                return fake_sftp

            def set_missing_host_key_policy(self, policy):
                return None

            def connect(self, *a, **kw):
                return None

            def close(self):
                return None

        fake_paramiko.SSHClient = lambda: _FakeSSH()
        fake_paramiko.AutoAddPolicy = lambda: None
        monkeypatch.setitem(sys.modules, "paramiko", fake_paramiko)

        runner = CliRunner()
        result = runner.invoke(resubmit_rejected_claims, [
            "--ingest-dir", str(tmp_path),
            "--no-validate",
            "--skip-edifabric-validate",
        ])
        assert result.exit_code == 0, result.output

        # Both files uploaded; the gate never fired.
        assert len(fake_sftp.put_calls) == 2, fake_sftp.put_calls
        assert validate_calls == [], (
            "validate must not be called when --skip-edifabric-validate is set"
        )
        assert "validate_failed=0" in result.output, result.output
        assert "EDIFABRIC VALIDATION FAIL" not in result.output, result.output
    finally:
        teardown()
