"""Assertion and cleanup steps for ``features/repo_indexing.feature``."""

from __future__ import annotations

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]

from cleveragents.application.services.repo_indexing_service import (
    RepoIndexingService,
)
from cleveragents.domain.models.core.repo_index import (
    FileRecord,
    IndexMetadata,
    RepoIndex,
)
from cleveragents.infrastructure.database.models import RepoIndexModel

_RESOURCE_ID = "01KK0D8WNATFNEX2JMG5GKF6FM"
_STALE_RESOURCE_ID = "01KK0D8WNATFNEX2JMG5GKF6FN"


@then('the repo-index result status should be "{status}"')
def step_check_status(context: Context, status: str) -> None:
    """Assert the index result status."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    actual = result.metadata.status.value
    assert actual == status, f"Expected status '{status}', got '{actual}'"


@then("the repo-index result file count should be {count:d}")
def step_check_file_count(context: Context, count: int) -> None:
    """Assert the file count."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    actual = result.metadata.file_count
    assert actual == count, (
        f"Expected {count} files, got {actual}. Files: {[f.path for f in result.files]}"
    )


@then("the repo-index result file count should be less than {count:d}")
def step_check_file_count_less(context: Context, count: int) -> None:
    """Assert the file count is less than a value."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    actual = result.metadata.file_count
    assert actual < count, f"Expected fewer than {count} files, got {actual}"


@then("the repo-index indexing elapsed seconds should be less than {seconds:d}")
def step_check_index_elapsed(context: Context, seconds: int) -> None:
    """Assert indexing runtime remained under the configured bound."""
    elapsed = context.repo_index_elapsed_seconds  # type: ignore[attr-defined]
    assert elapsed < float(seconds), (
        f"Expected elapsed < {seconds}s, got {elapsed:.2f}s"
    )


@then('the repo-index primary language should be "{lang}"')
def step_check_language(context: Context, lang: str) -> None:
    """Assert the primary language."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    actual = result.metadata.primary_language
    assert actual == lang, f"Expected language '{lang}', got '{actual}'"


@then("the repo-index token estimate should be positive")
def step_check_tokens_positive(context: Context) -> None:
    """Assert token estimate is > 0."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    assert result.metadata.token_estimate > 0, (
        f"Expected positive token_estimate, got {result.metadata.token_estimate}"
    )


@then('the repo-index file "{filename}" should have language "{lang}"')
def step_check_file_language(context: Context, filename: str, lang: str) -> None:
    """Assert a specific file's detected language."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    matches = [f for f in result.files if f.path == filename]
    assert matches, (
        f"File '{filename}' not found in index. Files: {[f.path for f in result.files]}"
    )
    actual = matches[0].language
    assert actual == lang, f"Expected '{filename}' language='{lang}', got '{actual}'"


@then("the repo-index retrieved index should match the original")
def step_check_retrieved_matches(context: Context) -> None:
    """Assert the retrieved index matches the original."""
    original: RepoIndex = context.repo_index_original_result  # type: ignore[attr-defined]
    retrieved: RepoIndex = context.repo_index_retrieved  # type: ignore[attr-defined]
    assert retrieved is not None, "Retrieved index is None"
    assert retrieved.metadata.file_count == original.metadata.file_count
    assert retrieved.metadata.token_estimate == original.metadata.token_estimate
    assert retrieved.metadata.primary_language == original.metadata.primary_language
    assert len(retrieved.files) == len(original.files)


@then("the repo-index retrieved file hashes should match the original")
def step_check_retrieved_hashes(context: Context) -> None:
    """Assert the retrieved index has identical per-file content hashes."""
    original: RepoIndex = context.repo_index_original_result  # type: ignore[attr-defined]
    retrieved: RepoIndex = context.repo_index_retrieved  # type: ignore[attr-defined]
    orig_map = {f.path: f.content_hash for f in original.files}
    retr_map = {f.path: f.content_hash for f in retrieved.files}
    assert orig_map == retr_map, (
        f"File hash mismatch.\nOriginal: {orig_map}\nRetrieved: {retr_map}"
    )


@then('the repo-index file "{filename}" content hash should differ from original')
def step_check_hash_differs(context: Context, filename: str) -> None:
    """Assert a file's hash changed after modification."""
    original: RepoIndex = context.repo_index_original_result  # type: ignore[attr-defined]
    current: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    orig_hash = next(
        (f.content_hash for f in original.files if f.path == filename),
        None,
    )
    curr_hash = next(
        (f.content_hash for f in current.files if f.path == filename),
        None,
    )
    assert orig_hash is not None, f"'{filename}' not found in original index"
    assert curr_hash is not None, f"'{filename}' not found in current index"
    assert orig_hash != curr_hash, (
        f"Expected hash for '{filename}' to change, but both are '{orig_hash}'"
    )


@then('the repo-index file "{filename}" content hash should match original')
def step_check_hash_matches(context: Context, filename: str) -> None:
    """Assert a file's hash is unchanged."""
    original: RepoIndex = context.repo_index_original_result  # type: ignore[attr-defined]
    current: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    orig_hash = next(
        (f.content_hash for f in original.files if f.path == filename),
        None,
    )
    curr_hash = next(
        (f.content_hash for f in current.files if f.path == filename),
        None,
    )
    assert orig_hash is not None, f"'{filename}' not found in original index"
    assert curr_hash is not None, f"'{filename}' not found in current index"
    assert orig_hash == curr_hash, (
        f"Expected hash for '{filename}' to match, but got '{orig_hash}' vs '{curr_hash}'"
    )


@then('the repo-index result should not include file "{filename}"')
def step_check_file_excluded(context: Context, filename: str) -> None:
    """Assert a file is not in the index."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    paths = [f.path for f in result.files]
    assert filename not in paths, f"File '{filename}' should be excluded but was found"


@then("a repo-index error should be raised")
def step_check_any_error(context: Context) -> None:
    """Assert some error was raised."""
    err = context.repo_index_error  # type: ignore[attr-defined]
    assert err is not None, "Expected an error but none raised"


@then('querying repo-index status for the resource should show "{status}"')
def step_check_error_status_persisted(context: Context, status: str) -> None:
    """Assert the persisted index status matches."""
    service: RepoIndexingService = context.repo_index_service  # type: ignore[attr-defined]
    meta = service.get_index_status(_RESOURCE_ID)
    assert meta is not None, (
        f"Expected persisted status '{status}' but get_index_status returned None"
    )
    actual = meta.status.value
    assert actual == status, f"Expected status '{status}', got '{actual}'"


@then("the repo-index previous index should still be retrievable with {count:d} files")
def step_check_previous_index(context: Context, count: int) -> None:
    """Assert the previous index is still intact."""
    service: RepoIndexingService = context.repo_index_service  # type: ignore[attr-defined]
    idx = service.get_index(_RESOURCE_ID)
    assert idx is not None, "Previous index was lost"
    assert idx.metadata.file_count == count, (
        f"Expected {count} files, got {idx.metadata.file_count}"
    )


@then("the FileRecord last_modified should have UTC timezone")
def step_check_filerecord_utc(context: Context) -> None:
    """Assert the FileRecord has UTC timezone on last_modified."""
    from datetime import UTC

    fr: FileRecord = context.repo_index_filerecord  # type: ignore[attr-defined]
    assert fr.last_modified.tzinfo is not None, "last_modified has no timezone"
    assert fr.last_modified.tzinfo == UTC, (
        f"Expected UTC, got {fr.last_modified.tzinfo}"
    )


@then("the IndexMetadata indexed_at should have UTC timezone")
def step_check_metadata_utc(context: Context) -> None:
    """Assert the IndexMetadata has UTC timezone on indexed_at."""
    from datetime import UTC

    meta: IndexMetadata = context.repo_index_metadata_naive  # type: ignore[attr-defined]
    assert meta.indexed_at.tzinfo is not None, "indexed_at has no timezone"
    assert meta.indexed_at.tzinfo == UTC, f"Expected UTC, got {meta.indexed_at.tzinfo}"


@then('the repo-index status metadata should have status "{status}"')
def step_check_status_metadata(context: Context, status: str) -> None:
    """Assert the status metadata has the expected status."""
    meta: IndexMetadata = context.repo_index_status  # type: ignore[attr-defined]
    assert meta is not None, "get_index_status returned None"
    actual = meta.status.value
    assert actual == status, f"Expected status '{status}', got '{actual}'"


@then("the repo-index status metadata file count should be {count:d}")
def step_check_status_file_count(context: Context, count: int) -> None:
    """Assert the status metadata file count."""
    meta: IndexMetadata = context.repo_index_status  # type: ignore[attr-defined]
    assert meta is not None, "get_index_status returned None"
    actual = meta.file_count
    assert actual == count, f"Expected {count} files, got {actual}"


@then("querying repo-index for the resource should return nothing")
def step_check_removed(context: Context) -> None:
    """Assert the index is gone."""
    service: RepoIndexingService = context.repo_index_service  # type: ignore[attr-defined]
    result = service.get_index(_RESOURCE_ID)
    assert result is None, f"Expected None after removal, got {result}"


@then("the repo-index removal result should be false")
def step_check_removal_false(context: Context) -> None:
    """Assert removal returned False."""
    assert context.repo_index_removal_result is False  # type: ignore[attr-defined]


@then('a repo-index ValueError should be raised mentioning "{text}"')
def step_check_value_error(context: Context, text: str) -> None:
    """Assert a ValueError was raised with expected message."""
    err = context.repo_index_error  # type: ignore[attr-defined]
    assert err is not None, "Expected ValueError but none raised"
    assert isinstance(err, ValueError), f"Expected ValueError, got {type(err).__name__}"
    assert text in str(err), f"Expected '{text}' in error: {err}"


@then("a repo-index FileNotFoundError should be raised")
def step_check_fnf_error(context: Context) -> None:
    """Assert a FileNotFoundError was raised."""
    err = context.repo_index_error  # type: ignore[attr-defined]
    assert err is not None, "Expected FileNotFoundError but none raised"
    assert isinstance(err, FileNotFoundError), (
        f"Expected FileNotFoundError, got {type(err).__name__}"
    )


@then('the repo-index metadata should have field "{field}"')
def step_check_metadata_field(context: Context, field: str) -> None:
    """Assert the metadata has a specific field."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    assert hasattr(result.metadata, field), (
        f"IndexMetadata missing field '{field}'. "
        f"Available: {list(result.metadata.model_fields.keys())}"
    )


@then('each repo-index file record should have field "{field}"')
def step_check_file_field(context: Context, field: str) -> None:
    """Assert every file record has a specific field."""
    result: RepoIndex = context.repo_index_result  # type: ignore[attr-defined]
    assert len(result.files) > 0, "No file records to check"
    for fr in result.files:
        assert hasattr(fr, field), (
            f"FileRecord missing field '{field}'. "
            f"Available: {list(fr.model_fields.keys())}"
        )


@given("a repo-index stale INDEXING row is inserted for a resource")
def step_insert_stale_indexing_row(context: Context) -> None:
    """Manually insert an INDEXING-status row to simulate a crash."""
    from datetime import UTC, datetime

    svc: RepoIndexingService = context.repo_index_service  # type: ignore[attr-defined]
    session = svc._session_factory()
    try:
        row = RepoIndexModel(
            index_id="01KK0D8WNATFNEX2JMG5GKF6FZ",
            resource_id=_STALE_RESOURCE_ID,
            file_count=0,
            token_estimate=0,
            primary_language="unknown",
            status="indexing",
            indexed_at=datetime.now(tz=UTC).isoformat(),
            created_at=datetime.now(tz=UTC).isoformat(),
        )
        session.add(row)
        session.commit()
    finally:
        session.close()
    context.repo_index_stale_resource_id = _STALE_RESOURCE_ID  # type: ignore[attr-defined]


@when("I call repo-index cleanup stale indexing")
def step_call_cleanup(context: Context) -> None:
    """Call cleanup_stale_indexing on the service."""
    svc: RepoIndexingService = context.repo_index_service  # type: ignore[attr-defined]
    context.repo_index_cleanup_count = svc.cleanup_stale_indexing()  # type: ignore[attr-defined]


@then("the repo-index cleanup should report {count:d} stale row removed")
def step_check_cleanup_count(context: Context, count: int) -> None:
    """Assert the cleanup returned the expected count."""
    actual = context.repo_index_cleanup_count  # type: ignore[attr-defined]
    assert actual == count, f"Expected {count} stale rows, got {actual}"


@then("querying repo-index for the stale resource should return nothing")
def step_check_stale_removed(context: Context) -> None:
    """Assert the stale row no longer exists."""
    svc: RepoIndexingService = context.repo_index_service  # type: ignore[attr-defined]
    rid = context.repo_index_stale_resource_id  # type: ignore[attr-defined]
    result = svc.get_index(rid)
    assert result is None, f"Expected None, got {result}"
