"""Tests for utm_builder.py - TDD: Write tests first, then implement."""

import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import patch

import pytest

# Import the module under test (will fail until implemented)
from utm_builder import (
    ALLOWED_CAMPAIGNS,
    ALLOWED_LANDING_DOMAINS,
    ALLOWED_MEDIUMS,
    ALLOWED_SOURCES,
    BatchItem,
    ValidationError,
    build_utm_url,
    process_batch,
    validate_params,
)


# ---------------------------------------------------------------------------
# 1. 올바른 값 입력 → 정상 URL 생성
# ---------------------------------------------------------------------------
class TestBuildUtmUrl:
    def test_basic_url_all_params(self) -> None:
        """source, medium, campaign, content, base 모두 정상 → 완성된 URL 반환."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="meta",
            medium="cpc",
            campaign="AB_A_snu",
            content="carousel_a1",
        )
        assert "utm_source=meta" in url
        assert "utm_medium=cpc" in url
        assert "utm_campaign=AB_A_snu" in url
        assert "utm_content=carousel_a1" in url
        assert url.startswith("https://incar-top.tistory.com")

    def test_url_without_optional_params(self) -> None:
        """content, term 없이도 정상 URL 생성."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="google",
            medium="display",
            campaign="org_move",
        )
        assert "utm_source=google" in url
        assert "utm_medium=display" in url
        assert "utm_campaign=org_move" in url
        assert "utm_content" not in url
        assert "utm_term" not in url

    def test_url_with_term(self) -> None:
        """term 파라미터 포함 URL 생성."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="naver_sa",
            medium="cpc",
            campaign="always_A",
            term="이직준비",
        )
        assert "utm_term=%EC%9D%B4%EC%A7%81%EC%A4%80%EB%B9%84" in url or "utm_term=" in url

    def test_url_with_all_optional_params(self) -> None:
        """content + term 모두 포함."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="kakao",
            medium="display",
            campaign="always_B",
            content="banner_01",
            term="전직",
        )
        assert "utm_content=banner_01" in url
        assert "utm_term=" in url

    def test_urgent_landing_page(self) -> None:
        """긴급 랜딩페이지 (incar-top1.tistory.com) 허용."""
        url = build_utm_url(
            base="https://incar-top1.tistory.com",
            source="meta",
            medium="social",
            campaign="urgent_A",
        )
        assert "incar-top1.tistory.com" in url
        assert "utm_source=meta" in url

    def test_base_url_with_existing_query(self) -> None:
        """base URL에 이미 쿼리스트링이 있는 경우에도 올바르게 처리."""
        url = build_utm_url(
            base="https://incar-top.tistory.com/entry/test?ref=home",
            source="meta",
            medium="cpc",
            campaign="AB_A_snu",
        )
        assert "utm_source=meta" in url
        assert "ref=home" in url


# ---------------------------------------------------------------------------
# 2. 잘못된 source → ValidationError
# ---------------------------------------------------------------------------
class TestValidateSource:
    def test_invalid_source_raises(self) -> None:
        """허용되지 않는 source → ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="invalid_source",
                medium="cpc",
                campaign="AB_A_snu",
                base="https://incar-top.tistory.com",
            )
        assert "utm_source" in str(exc_info.value).lower() or "source" in str(exc_info.value).lower()

    def test_invalid_source_message_contains_allowed_values(self) -> None:
        """에러 메시지에 허용 값 목록이 포함되어야 한다."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="facebook",
                medium="cpc",
                campaign="AB_A_snu",
                base="https://incar-top.tistory.com",
            )
        error_msg = str(exc_info.value)
        # 허용 값 중 하나 이상이 에러 메시지에 포함
        assert any(s in error_msg for s in ALLOWED_SOURCES)

    def test_valid_sources_pass(self) -> None:
        """모든 허용 source 값 검증 통과."""
        for source in ALLOWED_SOURCES:
            validate_params(
                source=source,
                medium="cpc",
                campaign="AB_A_snu",
                base="https://incar-top.tistory.com",
            )


# ---------------------------------------------------------------------------
# 3. 잘못된 medium → ValidationError
# ---------------------------------------------------------------------------
class TestValidateMedium:
    def test_invalid_medium_raises(self) -> None:
        """허용되지 않는 medium → ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="meta",
                medium="email",
                campaign="AB_A_snu",
                base="https://incar-top.tistory.com",
            )
        assert "medium" in str(exc_info.value).lower()

    def test_invalid_medium_message_contains_allowed_values(self) -> None:
        """에러 메시지에 허용 medium 목록 포함."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="meta",
                medium="organic",
                campaign="AB_A_snu",
                base="https://incar-top.tistory.com",
            )
        error_msg = str(exc_info.value)
        assert any(m in error_msg for m in ALLOWED_MEDIUMS)

    def test_valid_mediums_pass(self) -> None:
        """모든 허용 medium 값 검증 통과."""
        for medium in ALLOWED_MEDIUMS:
            validate_params(
                source="meta",
                medium=medium,
                campaign="AB_A_snu",
                base="https://incar-top.tistory.com",
            )


# ---------------------------------------------------------------------------
# 4. 잘못된 campaign → ValidationError
# ---------------------------------------------------------------------------
class TestValidateCampaign:
    def test_invalid_campaign_raises(self) -> None:
        """허용되지 않는 campaign → ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="meta",
                medium="cpc",
                campaign="random_campaign",
                base="https://incar-top.tistory.com",
            )
        assert "campaign" in str(exc_info.value).lower()

    def test_invalid_campaign_message_contains_allowed_values(self) -> None:
        """에러 메시지에 허용 campaign 목록 포함."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="meta",
                medium="cpc",
                campaign="test_campaign",
                base="https://incar-top.tistory.com",
            )
        error_msg = str(exc_info.value)
        assert any(c in error_msg for c in ALLOWED_CAMPAIGNS)

    def test_valid_campaigns_pass(self) -> None:
        """모든 허용 campaign 값 검증 통과."""
        for campaign in ALLOWED_CAMPAIGNS:
            validate_params(
                source="meta",
                medium="cpc",
                campaign=campaign,
                base="https://incar-top.tistory.com",
            )


# ---------------------------------------------------------------------------
# 5. 잘못된 base URL (허용되지 않은 도메인) → ValidationError
# ---------------------------------------------------------------------------
class TestValidateBaseUrl:
    def test_invalid_domain_raises(self) -> None:
        """허용되지 않는 도메인 → ValidationError."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="meta",
                medium="cpc",
                campaign="AB_A_snu",
                base="https://evil.com",
            )
        assert (
            "domain" in str(exc_info.value).lower()
            or "base" in str(exc_info.value).lower()
            or "landing" in str(exc_info.value).lower()
        )

    def test_invalid_domain_message_contains_allowed_domains(self) -> None:
        """에러 메시지에 허용 도메인 목록 포함."""
        with pytest.raises(ValidationError) as exc_info:
            validate_params(
                source="meta",
                medium="cpc",
                campaign="AB_A_snu",
                base="https://not-allowed.tistory.com",
            )
        error_msg = str(exc_info.value)
        assert any(d in error_msg for d in ALLOWED_LANDING_DOMAINS)

    def test_valid_domains_pass(self) -> None:
        """허용된 모든 도메인 검증 통과."""
        for domain in ALLOWED_LANDING_DOMAINS:
            validate_params(
                source="meta",
                medium="cpc",
                campaign="AB_A_snu",
                base=f"https://{domain}",
            )

    def test_invalid_url_format_raises(self) -> None:
        """URL 형식이 잘못된 경우 → ValidationError."""
        with pytest.raises(ValidationError):
            validate_params(
                source="meta",
                medium="cpc",
                campaign="AB_A_snu",
                base="not-a-url",
            )


# ---------------------------------------------------------------------------
# 6. content, term 선택 파라미터 동작 확인
# ---------------------------------------------------------------------------
class TestOptionalParams:
    def test_content_none_not_in_url(self) -> None:
        """content=None → utm_content 파라미터 없음."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="meta",
            medium="cpc",
            campaign="AB_A_snu",
            content=None,
        )
        assert "utm_content" not in url

    def test_term_none_not_in_url(self) -> None:
        """term=None → utm_term 파라미터 없음."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="meta",
            medium="cpc",
            campaign="AB_A_snu",
            term=None,
        )
        assert "utm_term" not in url

    def test_content_empty_string_not_in_url(self) -> None:
        """content='' → utm_content 파라미터 없음."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="meta",
            medium="cpc",
            campaign="AB_A_snu",
            content="",
        )
        assert "utm_content" not in url

    def test_term_with_content(self) -> None:
        """content + term 동시 포함."""
        url = build_utm_url(
            base="https://incar-top.tistory.com",
            source="naver_sa",
            medium="cpc",
            campaign="always_A",
            content="ad_01",
            term="keyword",
        )
        assert "utm_content=ad_01" in url
        assert "utm_term=keyword" in url


# ---------------------------------------------------------------------------
# 7. 배치 모드 JSON 입력 → 다량 URL 생성
# ---------------------------------------------------------------------------
class TestBatchMode:
    def test_batch_all_valid(self) -> None:
        """배치 항목 모두 정상 → 모든 URL 생성."""
        items: list[BatchItem] = [
            {
                "source": "meta",
                "medium": "cpc",
                "campaign": "AB_A_snu",
                "content": "carousel_a1",
                "base": "https://incar-top.tistory.com",
            },
            {
                "source": "google",
                "medium": "display",
                "campaign": "org_move",
                "base": "https://incar-top.tistory.com",
            },
        ]
        results = process_batch(items)
        assert len(results) == 2
        # 모든 결과가 성공 (error 없음)
        for result in results:
            assert result["error"] is None
            assert result["url"] is not None
            assert "utm_source=" in result["url"]

    def test_batch_url_params_correct(self) -> None:
        """배치 처리 결과 URL에 올바른 파라미터 포함."""
        items: list[BatchItem] = [
            {
                "source": "kakao",
                "medium": "social",
                "campaign": "always_B",
                "content": "img_01",
                "base": "https://incar-top.tistory.com",
            },
        ]
        results = process_batch(items)
        assert len(results) == 1
        url = results[0]["url"]
        assert url is not None
        assert "utm_source=kakao" in url
        assert "utm_medium=social" in url
        assert "utm_campaign=always_B" in url
        assert "utm_content=img_01" in url

    def test_batch_empty_list(self) -> None:
        """빈 배치 → 빈 결과 반환."""
        results = process_batch([])
        assert results == []


# ---------------------------------------------------------------------------
# 8. 배치 모드에서 일부 잘못된 항목 → 에러 표시 + 나머지 정상 처리
# ---------------------------------------------------------------------------
class TestBatchModePartialErrors:
    def test_batch_partial_invalid(self) -> None:
        """일부 항목 잘못된 경우 → 에러 항목은 error 필드, 정상 항목은 url 반환."""
        items: list[BatchItem] = [
            {
                "source": "meta",
                "medium": "cpc",
                "campaign": "AB_A_snu",
                "base": "https://incar-top.tistory.com",
            },
            {
                "source": "invalid_src",  # 잘못된 source
                "medium": "cpc",
                "campaign": "AB_A_snu",
                "base": "https://incar-top.tistory.com",
            },
            {
                "source": "google",
                "medium": "display",
                "campaign": "org_move",
                "base": "https://incar-top.tistory.com",
            },
        ]
        results = process_batch(items)
        assert len(results) == 3

        # 첫 번째: 정상
        assert results[0]["error"] is None
        assert results[0]["url"] is not None

        # 두 번째: 에러
        assert results[1]["error"] is not None
        assert results[1]["url"] is None

        # 세 번째: 정상
        assert results[2]["error"] is None
        assert results[2]["url"] is not None

    def test_batch_all_invalid(self) -> None:
        """모든 항목이 잘못된 경우 → 모두 에러."""
        items: list[BatchItem] = [
            {
                "source": "bad_source",
                "medium": "bad_medium",
                "campaign": "bad_campaign",
                "base": "https://incar-top.tistory.com",
            },
        ]
        results = process_batch(items)
        assert len(results) == 1
        assert results[0]["error"] is not None
        assert results[0]["url"] is None


# ---------------------------------------------------------------------------
# CLI 통합 테스트 (subprocess)
# ---------------------------------------------------------------------------
class TestCLI:
    SCRIPT = str(Path(os.environ.get("WORKSPACE_ROOT", str(Path(__file__).resolve().parent.parent))) / "scripts" / "utm_builder.py")

    def _run(self, args: list[str]) -> subprocess.CompletedProcess[str]:
        return subprocess.run(
            [sys.executable, self.SCRIPT, *args],
            capture_output=True,
            text=True,
        )

    def test_cli_valid_args_exit_0(self) -> None:
        """정상 인자 → exit code 0, stdout에 URL."""
        result = self._run(
            [
                "--source",
                "meta",
                "--medium",
                "cpc",
                "--campaign",
                "AB_A_snu",
                "--content",
                "carousel_a1",
                "--base",
                "https://incar-top.tistory.com",
            ]
        )
        assert result.returncode == 0
        assert "utm_source=meta" in result.stdout

    def test_cli_invalid_source_exit_1(self) -> None:
        """잘못된 source → exit code 1."""
        result = self._run(
            [
                "--source",
                "invalid",
                "--medium",
                "cpc",
                "--campaign",
                "AB_A_snu",
                "--base",
                "https://incar-top.tistory.com",
            ]
        )
        assert result.returncode == 1

    def test_cli_invalid_source_stderr_message(self) -> None:
        """잘못된 source → stderr에 에러 메시지 + 허용 값 목록."""
        result = self._run(
            [
                "--source",
                "invalid",
                "--medium",
                "cpc",
                "--campaign",
                "AB_A_snu",
                "--base",
                "https://incar-top.tistory.com",
            ]
        )
        # 에러 메시지가 stderr 또는 stdout에 출력
        combined = result.stderr + result.stdout
        assert "source" in combined.lower() or any(s in combined for s in ALLOWED_SOURCES)

    def test_cli_batch_mode(self) -> None:
        """--batch JSON 파일 → 다량 URL 출력."""
        batch_data = [
            {
                "source": "meta",
                "medium": "cpc",
                "campaign": "AB_A_snu",
                "content": "carousel_a1",
                "base": "https://incar-top.tistory.com",
            },
            {
                "source": "google",
                "medium": "display",
                "campaign": "org_move",
                "base": "https://incar-top.tistory.com",
            },
        ]
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(batch_data, f)
            tmp_path = f.name

        result = self._run(["--batch", tmp_path])
        assert result.returncode == 0
        assert "utm_source=meta" in result.stdout
        assert "utm_source=google" in result.stdout

        Path(tmp_path).unlink(missing_ok=True)

    def test_cli_batch_partial_error_exit_code(self) -> None:
        """배치 모드에서 일부 잘못된 항목 → exit code 1 (에러 있음)."""
        batch_data = [
            {
                "source": "meta",
                "medium": "cpc",
                "campaign": "AB_A_snu",
                "base": "https://incar-top.tistory.com",
            },
            {
                "source": "bad_src",
                "medium": "cpc",
                "campaign": "AB_A_snu",
                "base": "https://incar-top.tistory.com",
            },
        ]
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(batch_data, f)
            tmp_path = f.name

        result = self._run(["--batch", tmp_path])
        # 에러 항목이 있으면 exit code 1, 정상 URL은 출력
        assert result.returncode == 1
        assert "utm_source=meta" in result.stdout

        Path(tmp_path).unlink(missing_ok=True)
