"""Tests for performance checkers (PERF001-PERF002)."""

import pytest
from pathlib import Path

from stablestack.checkers.performance import (
    NPlusOneChecker,
    UninstrumentedLlmCallChecker,
)


class TestUninstrumentedLlmCallChecker:
    """Tests for PERF002: uninstrumented-llm-call."""

    @pytest.fixture
    def checker(self):
        return UninstrumentedLlmCallChecker()

    # --- Detection tests ---

    def test_detects_openai_chat(self, checker, temp_py_file):
        """Should detect OpenAI v1+ chat completions call."""
        code = 'response = client.chat.completions.create(model="gpt-4", messages=[])'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "PERF002"

    def test_detects_openai_legacy(self, checker, temp_py_file):
        """Should detect OpenAI legacy ChatCompletion call."""
        code = 'response = openai.ChatCompletion.create(model="gpt-4", messages=[])'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "PERF002"

    def test_detects_anthropic(self, checker, temp_py_file):
        """Should detect Anthropic messages.create call."""
        code = 'response = client.messages.create(model="claude-3", messages=[])'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "PERF002"

    def test_detects_openai_completions(self, checker, temp_py_file):
        """Should detect OpenAI completions.create call."""
        code = 'response = client.completions.create(model="gpt-3.5-turbo-instruct", prompt="hi")'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_detects_langchain_invoke(self, checker, temp_py_file):
        """Should detect LangChain .invoke() on chain-like objects."""
        code = 'result = chain.invoke({"input": "hello"})'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_detects_langchain_ainvoke(self, checker, temp_py_file):
        """Should detect LangChain .ainvoke() on model-like objects."""
        code = 'result = await llm_model.ainvoke(prompt)'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_detects_langchain_pipeline(self, checker, temp_py_file):
        """Should detect LangChain .invoke() on pipeline objects."""
        code = 'result = pipeline.invoke(data)'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_detects_langchain_agent(self, checker, temp_py_file):
        """Should detect LangChain .invoke() on agent objects."""
        code = 'result = agent_executor.invoke({"input": "query"})'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_does_not_flag_random_invoke(self, checker, temp_py_file):
        """Should NOT flag .invoke() on non-chain/llm objects."""
        code = 'result = handler.invoke(data)'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_detects_in_typescript(self, checker, temp_ts_file):
        """Should detect OpenAI calls in TypeScript files."""
        code = 'const response = await openai.chat.completions.create({ model: "gpt-4" });'
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_detects_in_javascript(self, checker, temp_js_file):
        """Should detect Anthropic calls in JavaScript files."""
        code = 'const msg = await client.messages.create({ model: "claude-3" });'
        path = temp_js_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_multiple_calls_in_file(self, checker, temp_py_file):
        """Should flag each uninstrumented call separately."""
        code = '''def process():
    r1 = client.chat.completions.create(model="gpt-4", messages=[])
    r2 = client.messages.create(model="claude-3", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 2

    # --- Suppression tests ---

    def test_suppresses_with_chattimer(self, checker, temp_py_file):
        """Should suppress when inside a `with ChatTimer.measure()` block."""
        code = '''from stablestack_timer import ChatTimer
timer = ChatTimer("test")
with timer.measure("llm"):
    response = client.chat.completions.create(model="gpt-4", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_suppresses_with_stablestack_timer(self, checker, temp_py_file):
        """Should suppress when inside a `with stablestack_timer` block."""
        code = '''with stablestack_timer.measure("call"):
    response = client.chat.completions.create(model="gpt-4", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_suppresses_with_generic_timer(self, checker, temp_py_file):
        """Should suppress when `with` block mentions timer."""
        code = '''with timer("llm_call"):
    response = client.chat.completions.create(model="gpt-4", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_suppresses_with_measure_context(self, checker, temp_py_file):
        """Should suppress when `with` block mentions measure."""
        code = '''with measure("api_call"):
    response = client.messages.create(model="claude-3", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_suppresses_timed_decorator(self, checker, temp_py_file):
        """Should suppress when function has @timed decorator."""
        code = '''@timed
def call_openai():
    response = client.chat.completions.create(model="gpt-4", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_suppresses_measure_decorator(self, checker, temp_py_file):
        """Should suppress when function has @measure decorator."""
        code = '''@measure
async def call_anthropic():
    response = client.messages.create(model="claude-3", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_suppresses_noqa_comment(self, checker, temp_py_file):
        """Should suppress when line has # noqa: PERF002."""
        code = 'response = client.chat.completions.create(model="gpt-4", messages=[])  # noqa: PERF002'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_skips_comment_lines(self, checker, temp_py_file):
        """Should not flag lines that are comments."""
        code = '# response = client.chat.completions.create(model="gpt-4", messages=[])'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_skips_js_comment_lines(self, checker, temp_ts_file):
        """Should not flag JS/TS comment lines."""
        code = '// const r = await openai.chat.completions.create({ model: "gpt-4" });'
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    # --- Edge cases ---

    def test_non_timer_with_block_still_flags(self, checker, temp_py_file):
        """Should still flag when inside a non-timer with block."""
        code = '''with open("output.txt") as f:
    response = client.chat.completions.create(model="gpt-4", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_stacked_decorators_with_timed(self, checker, temp_py_file):
        """Should suppress when @timed is among stacked decorators."""
        code = '''@retry(max_attempts=3)
@timed
def call_api():
    response = client.chat.completions.create(model="gpt-4", messages=[])
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_empty_file(self, checker, temp_py_file):
        """Should handle empty files gracefully."""
        code = ''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_llm_calls(self, checker, temp_py_file):
        """Should produce no findings when there are no LLM calls."""
        code = '''def process_data(items):
    for item in items:
        result = transform(item)
    return result
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_fix_hint_mentions_scaffold(self, checker, temp_py_file):
        """Fix hint should reference the scaffold command."""
        code = 'response = client.chat.completions.create(model="gpt-4", messages=[])'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert "stablestack add-llm-timing" in findings[0].fix_hint
