"""Unit tests for the scanner's detection internals.

Covers dependency parsing across requirements.txt / pyproject / package.json,
agent-metadata extraction, the model-name filter (prose vs real ids), the
catalog-skip heuristic, and MCP-config detection — over tiny in-memory repos.

Note: several tests here *document* the scanner's known static-detection limits
(the "catalog" and models.py skips the audit flagged). They assert the current
behaviour so a future change to it is a deliberate, visible decision.
"""
import json

from abom import scan as scanner
from abom.scan import _looks_like_real_model, parse_dependencies, _agent_meta


# --------------------------------------------------------------------------
# model-name filter
# --------------------------------------------------------------------------
def test_real_model_ids_accepted():
    assert _looks_like_real_model("claude-3-5-sonnet-20241022")
    assert _looks_like_real_model("gpt-4o-mini")
    assert _looks_like_real_model("mistral-large-2411")


def test_prose_and_envvars_rejected():
    assert not _looks_like_real_model("MISTRAL_API_KEY")       # env-var shape
    assert not _looks_like_real_model("claude-code")           # stopword segment
    assert not _looks_like_real_model("o3-vs-googles-gemini")  # stopword 'vs'
    # long alpha phrase reads as prose, not an id
    assert not _looks_like_real_model("some-really-long-model-name-phrase")


# --------------------------------------------------------------------------
# dependency parsing
# --------------------------------------------------------------------------
def test_parse_requirements_txt(tmp_path):
    (tmp_path / "requirements.txt").write_text(
        "langchain>=0.2\n# a comment\n-r other.txt\nopenai==1.5.0\n\nchromadb\n"
    )
    deps = parse_dependencies(tmp_path)
    assert deps["langchain"] == ">=0.2"       # range specs keep the operator
    assert deps["openai"] == "1.5.0"          # exact pins (==) drop the operator
    assert deps["chromadb"] == ""             # unpinned


def test_parse_pyproject_pep621_and_poetry(tmp_path):
    (tmp_path / "pyproject.toml").write_text(
        """
[project]
name = "myagent"
version = "2.3.0"
dependencies = ["anthropic>=0.30", "crewai"]

[tool.poetry.dependencies]
langgraph = "^0.1"
"""
    )
    deps = parse_dependencies(tmp_path)
    assert "anthropic" in deps
    assert "crewai" in deps
    assert "langgraph" in deps


def test_parse_package_json(tmp_path):
    (tmp_path / "package.json").write_text(json.dumps({
        "dependencies": {"openai": "^4.0.0"},
        "devDependencies": {"typescript": "5.0"},
    }))
    deps = parse_dependencies(tmp_path)
    assert deps["openai"] == "4.0.0"
    assert "typescript" in deps


def test_parse_malformed_files_are_tolerated(tmp_path):
    (tmp_path / "pyproject.toml").write_text("this is not [valid toml")
    (tmp_path / "package.json").write_text("{not json")
    # must not raise; just returns what it could parse (nothing here)
    assert isinstance(parse_dependencies(tmp_path), dict)


# --------------------------------------------------------------------------
# agent metadata
# --------------------------------------------------------------------------
def test_agent_meta_from_pyproject(tmp_path):
    (tmp_path / "pyproject.toml").write_text(
        '[project]\nname = "loan-agent"\nversion = "1.4.0"\n'
    )
    meta = _agent_meta(tmp_path)
    assert meta == {"name": "loan-agent", "version": "1.4.0"}


def test_agent_meta_from_package_json(tmp_path):
    (tmp_path / "package.json").write_text(json.dumps({"name": "js-agent", "version": "0.9"}))
    meta = _agent_meta(tmp_path)
    assert meta["name"] == "js-agent"
    assert meta["version"] == "0.9"


def test_agent_meta_defaults_to_dirname(tmp_path):
    meta = _agent_meta(tmp_path)
    assert meta["name"] == tmp_path.resolve().name
    assert meta["version"] == "0.0.0"


# --------------------------------------------------------------------------
# end-to-end scan of a tiny repo: models, tools, mcp config, catalog skip
# --------------------------------------------------------------------------
def test_scan_detects_model_tool_and_mcp(tmp_path):
    (tmp_path / "requirements.txt").write_text("langchain\nopenai\n")
    (tmp_path / "agent.py").write_text(
        "MODEL = 'gpt-4o-mini'\n"
        "from langchain_core.tools import tool\n"
        "@tool\ndef search_orders(q):\n    return q\n"
    )
    (tmp_path / "mcp.json").write_text(json.dumps({
        "mcpServers": {"filesystem": {}, "github": {}}
    }))
    result = scanner.scan(tmp_path)
    by_type = {}
    for c in result["components"]:
        by_type.setdefault(c["type"], set()).add(c.get("name"))
    assert "gpt-4o-mini" in by_type.get("model", set())
    assert "search_orders" in by_type.get("tool", set())
    assert {"filesystem", "github"} <= by_type.get("mcpServer", set())


def test_scan_skips_catalog_file(tmp_path):
    """A file named models.py (or a >8-model file) is treated as a catalog and
    its model names are NOT attributed — the documented static-scan limitation."""
    many = "\n".join(f"M{i} = 'gpt-4o-{i:02d}x'" for i in range(12))
    (tmp_path / "models.py").write_text("CATALOG = 'gpt-4o'\n" + many)
    result = scanner.scan(tmp_path)
    model_names = {c.get("name") for c in result["components"] if c["type"] == "model"}
    assert "gpt-4o" not in model_names  # models.py is skipped by name
