from datetime import datetime
from unittest.mock import AsyncMock, patch

import pytest
from fastapi import HTTPException

from app.middleware.trusted_context import TrustedContext
from app.models.chunk import ChunkPayload
from app.models.enums import LucosSourceType
from app.models.upload import ChunkUploadRequest
from app.models.workspace import WorkspaceRepoDocument
from app.services.upload_service import UploadService


@pytest.fixture
def ctx() -> TrustedContext:
    return TrustedContext(
        request_id="req-1",
        org_id="org-1",
        user_id="user-1",
        plan_code="pro",
    )


@pytest.fixture
def service() -> UploadService:
    svc = UploadService()
    svc._indexes_ready = True
    return svc


HASH_A = "sha256:aaaaaaaa"
HASH_B = "sha256:bbbbbbbb"


def _chunk(chunk_id: str, chunk_hash: str = HASH_A) -> ChunkPayload:
    return ChunkPayload(
        chunk_id=chunk_id,
        chunk_hash=chunk_hash,
        file_path="src/auth.ts",
        start_line=1,
        end_line=10,
        content="export const auth = true",
        source_type=LucosSourceType.REPO_CODE,
    )


@pytest.mark.asyncio
async def test_upload_skips_unchanged_chunks(service: UploadService, ctx: TrustedContext):
    body = ChunkUploadRequest(
        repo_id="lucos:ws:abc",
        chunks=[_chunk("c1")],
    )

    with (
        patch.object(service._repos, "get", AsyncMock(return_value=object())),
        patch.object(service._repos, "update_stats", AsyncMock()),
        patch.object(service._chunks, "get", AsyncMock(return_value=type("C", (), {"chunk_hash": HASH_A})())),
        patch.object(service._chunks, "count_for_repo", AsyncMock(return_value=1)),
        patch.object(service._chunks, "count_distinct_files", AsyncMock(return_value=1)),
        patch("app.services.upload_service.queue_chunk_embeddings", AsyncMock()) as embed_mock,
    ):
        result = await service.upload_chunks(body, ctx)

    assert result.counts.skipped == 1
    assert result.counts.indexed == 0
    embed_mock.assert_not_called()


@pytest.mark.asyncio
async def test_upload_indexes_new_chunks(service: UploadService, ctx: TrustedContext):
    body = ChunkUploadRequest(
        repo_id="lucos:ws:abc",
        chunks=[_chunk("c1")],
    )

    with (
        patch.object(service._repos, "get", AsyncMock(return_value=None)),
        patch.object(service._repos, "count_for_org", AsyncMock(return_value=0)),
        patch.object(service._repos, "create", AsyncMock()),
        patch.object(service._repos, "update_stats", AsyncMock()),
        patch.object(service._chunks, "get", AsyncMock(return_value=None)),
        patch.object(service._chunks, "upsert", AsyncMock()),
        patch.object(service._chunks, "count_for_repo", AsyncMock(return_value=1)),
        patch.object(service._chunks, "count_distinct_files", AsyncMock(return_value=1)),
        patch(
            "app.services.upload_service.queue_chunk_embeddings",
            AsyncMock(return_value={"failed": 0}),
        ) as embed_mock,
    ):
        result = await service.upload_chunks(body, ctx)

    assert result.counts.indexed == 1
    assert result.counts.skipped == 0
    embed_mock.assert_called_once()


@pytest.mark.asyncio
async def test_upload_registers_new_repo_when_under_limit(service: UploadService, ctx: TrustedContext):
    body = ChunkUploadRequest(
        repo_id="lucos:ws:new",
        chunks=[_chunk("c1")],
    )

    with (
        patch.object(service._repos, "get", AsyncMock(return_value=None)),
        patch.object(service._repos, "count_for_org", AsyncMock(return_value=0)),
        patch.object(service._repos, "create", AsyncMock()) as create_mock,
        patch.object(service._repos, "update_stats", AsyncMock()),
        patch.object(service._chunks, "get", AsyncMock(return_value=None)),
        patch.object(service._chunks, "upsert", AsyncMock()),
        patch.object(service._chunks, "count_for_repo", AsyncMock(return_value=1)),
        patch.object(service._chunks, "count_distinct_files", AsyncMock(return_value=1)),
        patch(
            "app.services.upload_service.queue_chunk_embeddings",
            AsyncMock(return_value={"failed": 0}),
        ),
    ):
        await service.upload_chunks(body, ctx)

    create_mock.assert_called_once()


@pytest.mark.asyncio
async def test_get_indexing_status_returns_repo_stats(
    service: UploadService,
    ctx: TrustedContext,
):
    indexed_at = datetime(2026, 7, 1, 12, 0, 0)
    repo = WorkspaceRepoDocument(
        org_id=ctx.org_id,
        repo_id="lucos:ws:abc",
        state="ready",
        files_indexed=3,
        chunks_total=9,
        stale_count=1,
        last_indexed_at=indexed_at,
    )

    with patch.object(service._repos, "get", AsyncMock(return_value=repo)):
        result = await service.get_indexing_status("lucos:ws:abc", ctx)

    assert result.repo_id == "lucos:ws:abc"
    assert result.state == "ready"
    assert result.files_indexed == 3
    assert result.chunks_total == 9
    assert result.stale_count == 1
    assert result.last_indexed_at == indexed_at.isoformat()


@pytest.mark.asyncio
async def test_get_indexing_status_returns_404_when_repo_missing(
    service: UploadService,
    ctx: TrustedContext,
):
    with patch.object(service._repos, "get", AsyncMock(return_value=None)):
        with pytest.raises(HTTPException) as exc_info:
            await service.get_indexing_status("lucos:ws:missing", ctx)

    assert exc_info.value.status_code == 404
    assert exc_info.value.detail["code"] == "REPO_NOT_FOUND"
