#!/usr/bin/env python3
"""Tests for diff-aware-qa.py — TDD (RED → GREEN)"""

import importlib.util
import json
import subprocess
import sys
import tempfile
import textwrap
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# diff-aware-qa.py 는 하이픈 파일명이므로 importlib 으로 임포트
_SCRIPTS_DIR = Path(__file__).parent.parent
_MODULE_PATH = _SCRIPTS_DIR / "diff-aware-qa.py"
spec = importlib.util.spec_from_file_location("diff_aware_qa", _MODULE_PATH)
assert spec is not None and spec.loader is not None
qa = importlib.util.module_from_spec(spec)
sys.modules["diff_aware_qa"] = qa
spec.loader.exec_module(qa)  # type: ignore[union-attr]


# ---------------------------------------------------------------------------
# 1. git diff 파싱 정확성
# ---------------------------------------------------------------------------
class TestParseGitDiff:
    def test_parse_normal_output(self):
        raw = "src/api/users.py\nfrontend/components/UserList.tsx\n"
        result = qa.parse_diff_output(raw)
        assert result == ["src/api/users.py", "frontend/components/UserList.tsx"]

    def test_parse_empty_output(self):
        result = qa.parse_diff_output("")
        assert result == []

    def test_parse_strips_whitespace(self):
        raw = "  src/api/users.py  \n  frontend/app.ts  \n"
        result = qa.parse_diff_output(raw)
        assert result == ["src/api/users.py", "frontend/app.ts"]

    def test_parse_ignores_blank_lines(self):
        raw = "file_a.py\n\nfile_b.ts\n\n"
        result = qa.parse_diff_output(raw)
        assert result == ["file_a.py", "file_b.ts"]


# ---------------------------------------------------------------------------
# 2. 파일 분류 정확성
# ---------------------------------------------------------------------------
class TestClassifyFiles:
    def test_classify_python_as_backend(self):
        cats = qa.classify_files(["src/routes/users.py"])
        assert "src/routes/users.py" in cats["backend"]

    def test_classify_ts_as_frontend(self):
        cats = qa.classify_files(["components/Button.ts"])
        assert "components/Button.ts" in cats["frontend"]

    def test_classify_tsx_as_frontend(self):
        cats = qa.classify_files(["components/Button.tsx"])
        assert "components/Button.tsx" in cats["frontend"]

    def test_classify_jsx_as_frontend(self):
        cats = qa.classify_files(["components/App.jsx"])
        assert "components/App.jsx" in cats["frontend"]

    def test_classify_css_as_style(self):
        cats = qa.classify_files(["styles/main.css"])
        assert "styles/main.css" in cats["style"]

    def test_classify_scss_as_style(self):
        cats = qa.classify_files(["styles/theme.scss"])
        assert "styles/theme.scss" in cats["style"]

    def test_classify_test_prefix_py(self):
        cats = qa.classify_files(["tests/test_users.py"])
        assert "tests/test_users.py" in cats["test"]

    def test_classify_dottest_ts(self):
        cats = qa.classify_files(["components/Button.test.ts"])
        assert "components/Button.test.ts" in cats["test"]

    def test_classify_other(self):
        cats = qa.classify_files(["config.json", "README.md"])
        assert "config.json" in cats["other"]
        assert "README.md" in cats["other"]

    def test_classify_mixed_files(self):
        files = ["api.py", "App.tsx", "styles.css", "tests/test_api.py", "config.json"]
        cats = qa.classify_files(files)
        assert "api.py" in cats["backend"]
        assert "App.tsx" in cats["frontend"]
        assert "styles.css" in cats["style"]
        assert "tests/test_api.py" in cats["test"]
        assert "config.json" in cats["other"]

    def test_classify_returns_all_keys(self):
        cats = qa.classify_files([])
        assert set(cats.keys()) == {"backend", "frontend", "style", "test", "other"}


# ---------------------------------------------------------------------------
# 3. 라우트 추출 — Flask / FastAPI 패턴
# ---------------------------------------------------------------------------
class TestExtractRoutes:
    def test_flask_route_decorator(self, tmp_path):
        f = tmp_path / "views.py"
        f.write_text(textwrap.dedent("""\
                @app.route('/api/users', methods=['GET'])
                def get_users():
                    pass
                """))
        routes = qa.extract_routes_from_file(str(f))
        assert "/api/users" in routes

    def test_fastapi_get_decorator(self, tmp_path):
        f = tmp_path / "router.py"
        f.write_text(textwrap.dedent("""\
                @router.get('/items/{item_id}')
                async def read_item(item_id: int):
                    pass
                """))
        routes = qa.extract_routes_from_file(str(f))
        assert "/items/{item_id}" in routes

    def test_fastapi_post_decorator(self, tmp_path):
        f = tmp_path / "router.py"
        f.write_text('@router.post("/users")\nasync def create_user(): pass\n')
        routes = qa.extract_routes_from_file(str(f))
        assert "/users" in routes

    def test_multiple_routes_in_file(self, tmp_path):
        f = tmp_path / "views.py"
        f.write_text(textwrap.dedent("""\
                @app.route('/api/users')
                def get_users(): pass

                @app.route('/api/products')
                def get_products(): pass
                """))
        routes = qa.extract_routes_from_file(str(f))
        assert "/api/users" in routes
        assert "/api/products" in routes

    def test_non_python_file_returns_empty(self, tmp_path):
        f = tmp_path / "component.tsx"
        f.write_text("export default function App() { return <div/>; }\n")
        routes = qa.extract_routes_from_file(str(f))
        assert routes == []

    def test_nonexistent_file_returns_empty(self):
        routes = qa.extract_routes_from_file("/nonexistent/path/file.py")
        assert routes == []


# ---------------------------------------------------------------------------
# 4. 컴포넌트명 추론
# ---------------------------------------------------------------------------
class TestExtractComponents:
    def test_export_default_function(self, tmp_path):
        f = tmp_path / "UserList.tsx"
        f.write_text("export default function UserList() { return null; }\n")
        comps = qa.extract_components_from_file(str(f))
        assert "UserList" in comps

    def test_export_default_const(self, tmp_path):
        f = tmp_path / "Button.tsx"
        f.write_text("export default const Button = () => null;\n")
        comps = qa.extract_components_from_file(str(f))
        assert "Button" in comps

    def test_fallback_to_filename(self, tmp_path):
        f = tmp_path / "MyWidget.tsx"
        f.write_text("// no explicit export default\nconst x = 1;\n")
        comps = qa.extract_components_from_file(str(f))
        assert "MyWidget" in comps

    def test_non_frontend_file_returns_empty(self, tmp_path):
        f = tmp_path / "utils.py"
        f.write_text("def helper(): pass\n")
        comps = qa.extract_components_from_file(str(f))
        assert comps == []

    def test_nonexistent_file_returns_empty(self):
        comps = qa.extract_components_from_file("/no/such/file.tsx")
        assert comps == []


# ---------------------------------------------------------------------------
# 5. 빈 diff → 빈 결과
# ---------------------------------------------------------------------------
class TestEmptyDiff:
    def test_empty_changed_files_gives_empty_analysis(self):
        result = qa.analyze_changes([])
        assert result["changed_files"] == []
        assert result["affected_routes"] == []
        assert result["affected_components"] == []
        for key in ("backend", "frontend", "style", "test", "other"):
            assert result["categories"][key] == []

    def test_empty_summary_string(self):
        result = qa.analyze_changes([])
        # 빈 diff 에서 요약은 비어있지 않은 문자열이어야 한다
        assert isinstance(result["summary"], str)
        assert len(result["summary"]) > 0


# ---------------------------------------------------------------------------
# 6. git 없는 디렉토리 → graceful 에러
# ---------------------------------------------------------------------------
class TestNonGitDirectory:
    def test_get_changed_files_non_git_dir(self, tmp_path):
        with pytest.raises(qa.GitError):
            qa.get_changed_files(project_dir=str(tmp_path), base_ref="main")

    def test_get_changed_files_returns_error_message(self, tmp_path):
        try:
            qa.get_changed_files(project_dir=str(tmp_path), base_ref="main")
        except qa.GitError as exc:
            assert str(exc)  # 에러 메시지가 비어있지 않음


# ---------------------------------------------------------------------------
# 7. 다양한 base-ref 처리
# ---------------------------------------------------------------------------
class TestBaseRef:
    def test_custom_base_ref_used_in_command(self):
        """get_changed_files 가 올바른 base-ref 로 git 명령을 조합하는지 검증"""
        with patch("diff_aware_qa.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout="file.py\n", stderr="")
            with tempfile.TemporaryDirectory() as tmpdir:
                # 임시로 git repo 처럼 보이게 만들기
                Path(tmpdir, ".git").mkdir()
                qa.get_changed_files(project_dir=tmpdir, base_ref="develop")
            # called_cmd 는 리스트이므로 any() 로 검사
            called_cmd = mock_run.call_args[0][0]
            assert any("develop" in part for part in called_cmd)

    def test_default_base_ref_is_main(self):
        with patch("diff_aware_qa.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
            with tempfile.TemporaryDirectory() as tmpdir:
                Path(tmpdir, ".git").mkdir()
                qa.get_changed_files(project_dir=tmpdir)
            called_cmd = mock_run.call_args[0][0]
            assert any("main" in part for part in called_cmd)


# ---------------------------------------------------------------------------
# 8. JSON 출력 스키마 검증
# ---------------------------------------------------------------------------
class TestJsonOutputSchema:
    REQUIRED_KEYS = {
        "changed_files",
        "affected_routes",
        "affected_components",
        "categories",
        "qa_targets",
        "summary",
    }
    CATEGORY_KEYS = {"backend", "frontend", "style", "test", "other"}

    def test_analyze_changes_has_all_top_level_keys(self):
        result = qa.analyze_changes([])
        assert self.REQUIRED_KEYS <= set(result.keys())

    def test_analyze_changes_categories_has_all_keys(self):
        result = qa.analyze_changes([])
        assert self.CATEGORY_KEYS <= set(result["categories"].keys())

    def test_changed_files_is_list(self):
        result = qa.analyze_changes(["a.py"])
        assert isinstance(result["changed_files"], list)

    def test_affected_routes_is_list(self):
        result = qa.analyze_changes([])
        assert isinstance(result["affected_routes"], list)

    def test_affected_components_is_list(self):
        result = qa.analyze_changes([])
        assert isinstance(result["affected_components"], list)

    def test_qa_targets_is_list(self):
        result = qa.analyze_changes([])
        assert isinstance(result["qa_targets"], list)

    def test_summary_is_string(self):
        result = qa.analyze_changes([])
        assert isinstance(result["summary"], str)

    def test_json_serializable(self):
        result = qa.analyze_changes(["api.py", "App.tsx"])
        dumped = json.dumps(result)
        reloaded = json.loads(dumped)
        assert reloaded["changed_files"] == result["changed_files"]


# ---------------------------------------------------------------------------
# 9. build_summary 헬퍼
# ---------------------------------------------------------------------------
class TestBuildSummary:
    def test_summary_mentions_backend_count(self):
        cats = {"backend": ["a.py", "b.py"], "frontend": [], "style": [], "test": [], "other": []}
        s = qa.build_summary(cats)
        assert "2" in s

    def test_summary_mentions_frontend_count(self):
        cats = {"backend": [], "frontend": ["A.tsx", "B.tsx", "C.tsx"], "style": [], "test": [], "other": []}
        s = qa.build_summary(cats)
        assert "3" in s

    def test_summary_zero_changes(self):
        cats = {"backend": [], "frontend": [], "style": [], "test": [], "other": []}
        s = qa.build_summary(cats)
        assert isinstance(s, str)


# ---------------------------------------------------------------------------
# 10. analyze_changes 통합
# ---------------------------------------------------------------------------
class TestAnalyzeChangesIntegration:
    def test_backend_files_populate_routes(self, tmp_path):
        f = tmp_path / "views.py"
        f.write_text("@app.route('/ping')\ndef ping(): pass\n")
        result = qa.analyze_changes([str(f)])
        assert "/ping" in result["affected_routes"]

    def test_frontend_files_populate_components(self, tmp_path):
        f = tmp_path / "Card.tsx"
        f.write_text("export default function Card() { return null; }\n")
        result = qa.analyze_changes([str(f)])
        assert "Card" in result["affected_components"]

    def test_qa_targets_non_empty_for_changed_files(self, tmp_path):
        f = tmp_path / "api.py"
        f.write_text("@app.route('/health')\ndef health(): pass\n")
        result = qa.analyze_changes([str(f)])
        assert len(result["qa_targets"]) > 0
