"""Step definitions for context_analysis_new_coverage.feature."""

from __future__ import annotations

import tempfile
from typing import Any
from unittest.mock import MagicMock

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]
from langchain_community.llms import FakeListLLM
from langchain_core.documents import Document

from cleveragents.agents.graphs.context_analysis import (
    ContextAnalysisAgent,
    ContextAnalysisState,
)


def _default_context_test_llm() -> FakeListLLM:
    """Create a FakeListLLM for context analysis test purposes."""
    return FakeListLLM(
        responses=[
            "Dependencies: ['os', 'sys', 'pathlib']",
            "Relevance: High - contains core functionality",
            "Summary: Python module implementing core business logic",
        ]
    )


def _empty_state(**overrides: Any) -> ContextAnalysisState:
    base: ContextAnalysisState = {
        "file_paths": [],
        "documents": [],
        "dependencies": {},
        "summary": "",
        "relevance_scores": {},
        "chunks": [],
        "error": None,
    }
    base.update(overrides)  # type: ignore[typeddict-item]
    return base


@when("I create a ContextAnalysisAgent without an LLM")
def step_create_default(context: Context) -> None:
    context.agent = ContextAnalysisAgent(llm=_default_context_test_llm())


@when("I create a new ContextAnalysisAgent without an LLM expecting error")
def step_create_no_llm_error(context: Context) -> None:
    context.raised_error = None
    try:
        ContextAnalysisAgent(llm=None)
    except ValueError as exc:
        context.raised_error = exc


@then("a ValueError should be raised about missing LLM")
def step_assert_value_error_missing_llm(context: Context) -> None:
    assert context.raised_error is not None, "Expected ValueError but none was raised"
    assert "No LLM provider configured" in str(context.raised_error)


@then("the agent should use FakeListLLM")
def step_assert_fake_llm(context: Context) -> None:
    assert isinstance(context.agent.llm, FakeListLLM)


@then("the agent should have a compiled graph")
def step_assert_compiled(context: Context) -> None:
    assert context.agent.app is not None


@when("I create a ContextAnalysisAgent with a mock LLM")
def step_create_with_llm(context: Context) -> None:
    custom_llm = FakeListLLM(responses=["custom response"])
    context.agent = ContextAnalysisAgent(llm=custom_llm)


@then("the agent should use the provided LLM")
def step_assert_custom_llm(context: Context) -> None:
    assert isinstance(context.agent.llm, FakeListLLM)


@given("a ContextAnalysisAgent instance")
def step_agent_instance(context: Context) -> None:
    context.agent = ContextAnalysisAgent(llm=_default_context_test_llm())


@when("I invoke _load_files with preloaded documents")
def step_load_preloaded(context: Context) -> None:
    doc = Document(page_content="hello world", metadata={"source": "test.py"})
    state = _empty_state(documents=[doc])
    context.result = context.agent._load_files(state)


@then("the result should return those preloaded documents")
def step_assert_preloaded(context: Context) -> None:
    assert len(context.result["documents"]) == 1


@given("a temporary Python file on disk")
def step_temp_py_file(context: Context) -> None:
    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".py", delete=False, encoding="utf-8"
    ) as tmp:
        tmp.write("import os\nimport sys\n\ndef main():\n    pass\n")
        tmp.flush()
        context.temp_file = tmp.name


@when("I invoke _load_files with the temp file path")
def step_load_temp(context: Context) -> None:
    state = _empty_state(file_paths=[context.temp_file])
    context.result = context.agent._load_files(state)


@then("the result should contain loaded documents")
def step_assert_loaded(context: Context) -> None:
    assert len(context.result["documents"]) > 0


@when("I invoke _load_files with a nonexistent file path")
def step_load_missing(context: Context) -> None:
    state = _empty_state(file_paths=["/tmp/nonexistent_file_xyz.py"])
    context.result = context.agent._load_files(state)


@then("the result error should mention file not found")
def step_assert_file_not_found(context: Context) -> None:
    assert context.result["error"] is not None
    assert "not found" in context.result["error"].lower()


@when("I invoke _load_files with a directory path")
def step_load_directory(context: Context) -> None:
    state = _empty_state(file_paths=["/tmp"])
    context.result = context.agent._load_files(state)


@then("the result error should mention not a file")
def step_assert_not_file(context: Context) -> None:
    assert context.result["error"] is not None
    assert "not a file" in context.result["error"].lower()


@given("a state with one loaded document")
def step_one_doc_state(context: Context) -> None:
    doc = Document(
        page_content="import os\nimport sys\n", metadata={"source": "test.py"}
    )
    context.state = _empty_state(documents=[doc])


@when("I invoke _analyze_dependencies")
def step_analyze_deps(context: Context) -> None:
    context.result = context.agent._analyze_dependencies(context.state)


@then("the dependencies dict should have entries")
def step_assert_deps(context: Context) -> None:
    assert len(context.result["dependencies"]) > 0


@when('I parse dependencies from "{text}"')
def step_parse_deps(context: Context, text: str) -> None:
    context.result = context.agent._parse_dependencies(text)


@then("the parsed list should contain os sys pathlib")
def step_assert_parsed_deps(context: Context) -> None:
    deps_lower = [d.lower() for d in context.result]
    for name in ("os", "sys", "pathlib"):
        assert any(name in d for d in deps_lower), (
            f"'{name}' not found in {context.result}"
        )


@given("a state with a small document")
def step_small_doc(context: Context) -> None:
    doc = Document(page_content="short content", metadata={"source": "small.py"})
    context.state = _empty_state(documents=[doc])


@when("I invoke _chunk_documents")
def step_chunk_docs(context: Context) -> None:
    context.result = context.agent._chunk_documents(context.state)


@then("the chunks should contain the original document")
def step_assert_original_chunk(context: Context) -> None:
    assert len(context.result["chunks"]) == 1


@given("a ContextAnalysisAgent instance with chunk_size {size:d}")
def step_agent_custom_chunk(context: Context, size: int) -> None:
    context.agent = ContextAnalysisAgent(
        llm=_default_context_test_llm(), chunk_size=size, chunk_overlap=20
    )


@given("a state with a large document of {n:d} characters")
def step_large_doc(context: Context, n: int) -> None:
    doc = Document(page_content="x" * n, metadata={"source": "large.py"})
    context.state = _empty_state(documents=[doc])


@then("the chunks should contain more than one document")
def step_assert_multiple_chunks(context: Context) -> None:
    assert len(context.result["chunks"]) > 1


@given("a state with chunks from two files")
def step_chunks_two_files(context: Context) -> None:
    chunks = [
        Document(page_content="code a", metadata={"source": "a.py"}),
        Document(page_content="code b", metadata={"source": "b.py"}),
    ]
    context.state = _empty_state(chunks=chunks)


@when("I invoke _score_relevance")
def step_score_relevance(context: Context) -> None:
    context.result = context.agent._score_relevance(context.state)


@then("relevance scores should exist for both files")
def step_assert_both_scores(context: Context) -> None:
    scores = context.result["relevance_scores"]
    assert len(scores) == 2


@when('I parse relevance score from "{text}"')
def step_parse_score(context: Context, text: str) -> None:
    context.parsed_score = context.agent._parse_relevance_score(text)


@then("the parsed score should be approximately {expected}")
def step_assert_approx_score(context: Context, expected: str) -> None:
    assert abs(context.parsed_score - float(expected)) < 0.1, (
        f"Expected ~{expected}, got {context.parsed_score}"
    )


@given("a full analysis state with documents and scores")
def step_full_state(context: Context) -> None:
    doc = Document(page_content="import os\n", metadata={"source": "main.py"})
    context.state = _empty_state(
        documents=[doc],
        dependencies={"main.py": ["os"]},
        relevance_scores={"main.py": 0.9},
        chunks=[doc],
    )


@when("I invoke _summarize_context")
def step_summarize(context: Context) -> None:
    context.result = context.agent._summarize_context(context.state)


@then("the direct summary should be a non-empty string")
def step_assert_summary(context: Context) -> None:
    assert context.result["summary"]


@when("I invoke the full workflow with the temp file")
def step_invoke_full(context: Context) -> None:
    state = _empty_state(file_paths=[context.temp_file])
    config = {"configurable": {"thread_id": "test-invoke"}}
    context.result = context.agent.invoke(state, config)


@then("the result should contain summary and scores")
def step_assert_full_result(context: Context) -> None:
    assert "summary" in context.result
    assert "relevance_scores" in context.result


@when("I stream the workflow with the temp file")
def step_stream(context: Context) -> None:
    state = _empty_state(file_paths=[context.temp_file])
    config = {"configurable": {"thread_id": "test-stream"}}
    context.events = list(context.agent.stream(state, config))


@then("at least one analysis event should be yielded")
def step_assert_events(context: Context) -> None:
    assert len(context.events) > 0


@when("I wrap a chain with retry support")
def step_with_retry(context: Context) -> None:
    mock_chain = MagicMock()
    mock_chain.with_retry.return_value = mock_chain
    context.result = context.agent._with_retry(mock_chain)


@then("the wrapped chain should be returned")
def step_assert_wrapped(context: Context) -> None:
    assert context.result is not None
