import asyncio

from app.models.chunk import DocChunk
from app.models.embedding_job import EmbeddingJob, EmbeddingJobStatus
from app.services import embedding_job_service


def _chunk(**overrides) -> DocChunk:
    payload = {
        "chunk_id": "chk_1",
        "repo_id": "ad/repo",
        "doc_path": "docs/api.md",
        "section_title": "API",
        "chunk_text": "Body text",
        "chunk_hash": "hash_1",
        "source_type": "repo_doc",
        "metadata": {
            "chunk_type": "text",
            "section_level": 1,
            "chunk_order": 0,
            "section_index": 0,
            "chunk_index": 0,
        },
    }
    payload.update(overrides)
    return DocChunk(**payload)


def test_build_embedding_jobs_is_deterministic_and_traceable():
    chunks = [_chunk(chunk_id="chk_2", chunk_hash="hash_2", metadata={"chunk_type": "text", "section_level": 1, "chunk_order": 2}), _chunk(chunk_id="chk_1", chunk_hash="hash_1", metadata={"chunk_type": "text", "section_level": 1, "chunk_order": 1})]

    jobs1 = embedding_job_service.build_embedding_jobs(
        chunks,
        document_id="doc_1",
        snapshot_id="snap_1",
    )
    jobs2 = embedding_job_service.build_embedding_jobs(
        chunks,
        document_id="doc_1",
        snapshot_id="snap_1",
    )

    assert [j.job_id for j in jobs1] == [j.job_id for j in jobs2]
    assert [j.chunk_id for j in jobs1] == ["chk_1", "chk_2"]
    assert jobs1[0].snapshot_id == "snap_1"
    assert jobs1[0].document_id == "doc_1"
    assert jobs1[0].status == EmbeddingJobStatus.PENDING
    assert jobs1[0].repo_id == "ad/repo"
    assert jobs1[0].source_type == "repo_doc"
    assert jobs1[0].chunk_type == "text"
    assert jobs1[0].payload["metadata"]["chunk_order"] == 1


def test_resolve_snapshot_id_prefers_snapshot_then_commit():
    assert embedding_job_service.resolve_snapshot_id("snap_1", "abc") == "snap_1"
    assert embedding_job_service.resolve_snapshot_id("", "abc") == "commit:abc"
    assert embedding_job_service.resolve_snapshot_id(None, None) == "snapshot:unknown"


def test_create_embedding_jobs_uses_upsert_repo(monkeypatch):
    called = {}

    async def _upsert_jobs(jobs):
        called["jobs"] = jobs
        return {"requested": len(jobs), "upserted": 1}

    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "upsert_jobs", _upsert_jobs)

    summary = asyncio.run(
        embedding_job_service.create_embedding_jobs(
            [_chunk()],
            document_id="doc_1",
            snapshot_id="snap_1",
        )
    )

    assert summary == {"requested": 1, "upserted": 1}
    assert len(called["jobs"]) == 1
    assert called["jobs"][0].chunk_id == "chk_1"


def test_dispatch_embedding_jobs_transitions_lifecycle(monkeypatch):
    job = EmbeddingJob(
        job_id="emb_1",
        dedupe_key="chk_1:snap_1",
        chunk_id="chk_1",
        repo_id="ad/repo",
        snapshot_id="snap_1",
        document_id="doc_1",
        doc_path="docs/api.md",
        source_type="repo_doc",
        chunk_type="text",
        payload={"chunk_id": "chk_1"},
        status=EmbeddingJobStatus.PENDING,
    )
    states = []

    async def _get_jobs(_statuses, limit=100, ready_only=False):
        assert limit == 10
        assert ready_only is True
        return [job]

    async def _update_state(job_id, status, **kwargs):
        states.append((job_id, status, kwargs))

    async def _publish(_job):
        return None

    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "get_jobs_by_status", _get_jobs)
    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "update_job_state", _update_state)
    monkeypatch.setattr(embedding_job_service, "_publish_job", _publish)

    summary = asyncio.run(embedding_job_service.dispatch_embedding_jobs(limit=10))

    assert summary == {"processed": 1, "dispatched": 1, "failed": 0}
    assert states[0][1] == EmbeddingJobStatus.PROCESSING
    assert states[1][1] == EmbeddingJobStatus.COMPLETED


def test_dispatch_embedding_jobs_retries_on_failure(monkeypatch):
    job = EmbeddingJob(
        job_id="emb_1",
        dedupe_key="chk_1:snap_1",
        chunk_id="chk_1",
        repo_id="ad/repo",
        snapshot_id="snap_1",
        document_id="doc_1",
        doc_path="docs/api.md",
        source_type="repo_doc",
        chunk_type="text",
        payload={"chunk_id": "chk_1"},
        status=EmbeddingJobStatus.PENDING,
        retry_count=0,
    )
    states = []

    async def _get_jobs(_statuses, limit=100, ready_only=False):
        assert ready_only is True
        return [job]

    async def _update_state(job_id, status, **kwargs):
        states.append((job_id, status, kwargs))

    async def _publish(_job):
        raise RuntimeError("redis down")

    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "get_jobs_by_status", _get_jobs)
    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "update_job_state", _update_state)
    monkeypatch.setattr(embedding_job_service, "_publish_job", _publish)

    summary = asyncio.run(embedding_job_service.dispatch_embedding_jobs(limit=10))

    assert summary == {"processed": 1, "dispatched": 0, "failed": 1}
    assert states[0][1] == EmbeddingJobStatus.PROCESSING
    assert states[1][1] == EmbeddingJobStatus.RETRYING
    assert states[1][2]["next_retry_at"] is not None
    assert states[1][2]["retry_event"]["backoff_seconds"] >= 1
    assert states[1][2]["retry_event"]["retryable"] is True


def test_dispatch_embedding_jobs_marks_failed_for_non_retryable_errors(monkeypatch):
    job = EmbeddingJob(
        job_id="emb_1",
        dedupe_key="chk_1:snap_1",
        chunk_id="chk_1",
        repo_id="ad/repo",
        snapshot_id="snap_1",
        document_id="doc_1",
        doc_path="docs/api.md",
        source_type="repo_doc",
        chunk_type="text",
        payload={"chunk_id": "chk_1"},
        status=EmbeddingJobStatus.PENDING,
        retry_count=0,
    )
    states = []

    async def _get_jobs(_statuses, limit=100, ready_only=False):
        return [job]

    async def _update_state(job_id, status, **kwargs):
        states.append((job_id, status, kwargs))

    async def _publish(_job):
        raise RuntimeError("Unsupported embedding publish backend: bad")

    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "get_jobs_by_status", _get_jobs)
    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "update_job_state", _update_state)
    monkeypatch.setattr(embedding_job_service, "_publish_job", _publish)

    summary = asyncio.run(embedding_job_service.dispatch_embedding_jobs(limit=10))

    assert summary == {"processed": 1, "dispatched": 0, "failed": 1}
    assert states[1][1] == EmbeddingJobStatus.FAILED
    assert states[1][2]["clear_next_retry"] is True
    assert states[1][2]["retry_event"]["retryable"] is False


def test_retry_failed_jobs_marks_retrying(monkeypatch):
    job = EmbeddingJob(
        job_id="emb_1",
        dedupe_key="chk_1:snap_1",
        chunk_id="chk_1",
        repo_id="ad/repo",
        snapshot_id="snap_1",
        document_id="doc_1",
        doc_path="docs/api.md",
        source_type="repo_doc",
        chunk_type="text",
        payload={"chunk_id": "chk_1"},
        status=EmbeddingJobStatus.FAILED,
    )
    states = []

    async def _get_jobs(_statuses, limit=100):
        return [job]

    async def _update_state(job_id, status, **kwargs):
        states.append((job_id, status, kwargs))

    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "get_jobs_by_status", _get_jobs)
    monkeypatch.setattr(embedding_job_service.embedding_job_repo, "update_job_state", _update_state)

    count = asyncio.run(embedding_job_service.retry_failed_jobs(limit=5))

    assert count == 1
    assert states[0][1] == EmbeddingJobStatus.RETRYING


def test_retry_backoff_seconds_is_deterministic(monkeypatch):
    monkeypatch.setattr(
        embedding_job_service,
        "env_int",
        lambda name, default: 5 if name == "EMBEDDING_JOB_RETRY_BASE_SECONDS" else (60 if name == "EMBEDDING_JOB_RETRY_MAX_SECONDS" else default),
    )

    assert embedding_job_service._retry_backoff_seconds(0) == 5
    assert embedding_job_service._retry_backoff_seconds(1) == 10
    assert embedding_job_service._retry_backoff_seconds(2) == 20
    assert embedding_job_service._retry_backoff_seconds(10) == 60


def test_publish_job_uses_selected_transport(monkeypatch):
    job = EmbeddingJob(
        job_id="emb_1",
        dedupe_key="chk_1:snap_1",
        chunk_id="chk_1",
        repo_id="ad/repo",
        snapshot_id="snap_1",
        document_id="doc_1",
        doc_path="docs/api.md",
        source_type="repo_doc",
        chunk_type="text",
        payload={
            "chunk_id": "chk_1",
            "repo_id": "ad/repo",
            "snapshot_id": "snap_1",
            "document_id": "doc_1",
            "source_type": "repo_doc",
            "chunk_type": "text",
            "doc_path": "docs/api.md",
            "section_title": "API",
            "section_level": 1,
            "chunk_text": "Body text",
            "chunk_hash": "hash_1",
            "metadata": {"chunk_order": 0},
        },
        status=EmbeddingJobStatus.PENDING,
    )
    published = {}

    class _Publisher:
        async def publish(self, event):
            published["event"] = event

    monkeypatch.setattr(embedding_job_service, "env_str", lambda name, default="": "kafka" if name == "EMBEDDING_JOB_PUBLISH_BACKEND" else default)
    monkeypatch.setattr(embedding_job_service, "get_embedding_publisher", lambda _backend: _Publisher())

    asyncio.run(embedding_job_service._publish_job(job))

    assert published["event"]["job_id"] == "emb_1"
    assert published["event"]["repo_id"] == "ad/repo"
    assert published["event"]["snapshot_id"] == "snap_1"
    assert published["event"]["document_id"] == "doc_1"
    assert published["event"]["chunk_id"] == "chk_1"


def test_build_publish_event_serializes_payload_deterministically():
    job = EmbeddingJob(
        job_id="emb_1",
        dedupe_key="chk_1:snap_1",
        chunk_id="chk_1",
        repo_id="ad/repo",
        snapshot_id="snap_1",
        document_id="doc_1",
        doc_path="docs/api.md",
        source_type="repo_doc",
        chunk_type="text",
        payload={
            "chunk_id": "chk_1",
            "repo_id": "ad/repo",
            "snapshot_id": "snap_1",
            "document_id": "doc_1",
            "source_type": "repo_doc",
            "chunk_type": "text",
            "doc_path": "docs/api.md",
            "section_title": "API",
            "section_level": 1,
            "chunk_text": "Body text",
            "chunk_hash": "hash_1",
            "metadata": {"b": 1, "a": 2},
        },
        status=EmbeddingJobStatus.PENDING,
    )

    event = embedding_job_service._build_publish_event(job)

    assert '"chunk_id":"chk_1"' in event["payload"]
    assert '"metadata":{"a":2,"b":1}' in event["payload"]
