# -*- coding: utf-8 -*-
"""CLIBuilder 单元测试

覆盖：7 种 Flow、4 种处理模式、时间窗口、store_id 自动注入、extra_args 等。
"""

import pytest

from app.schemas.tasks import TaskConfigSchema
from app.services.cli_builder import CLIBuilder, VALID_FLOWS, VALID_PROCESSING_MODES


@pytest.fixture
def builder() -> CLIBuilder:
    return CLIBuilder()


ETL_PATH = "/fake/etl/project"


# ---------------------------------------------------------------------------
# 基本命令结构
# ---------------------------------------------------------------------------

class TestBasicCommand:
    def test_minimal_command(self, builder: CLIBuilder):
        """最小配置应生成 python -m cli.main --flow ... --processing-mode ..."""
        config = TaskConfigSchema(tasks=["ODS_MEMBER"])
        cmd = builder.build_command(config, ETL_PATH)
        assert cmd[:3] == ["python", "-m", "cli.main"]
        assert "--flow" in cmd
        assert "--processing-mode" in cmd

    def test_custom_python_executable(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"])
        cmd = builder.build_command(config, ETL_PATH, python_executable="python3")
        assert cmd[0] == "python3"

    def test_tasks_joined_by_comma(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER", "ODS_PAYMENT", "ODS_REFUND"])
        cmd = builder.build_command(config, ETL_PATH)
        idx = cmd.index("--tasks")
        assert cmd[idx + 1] == "ODS_MEMBER,ODS_PAYMENT,ODS_REFUND"

    def test_empty_tasks_no_tasks_arg(self, builder: CLIBuilder):
        """空任务列表不应生成 --tasks 参数"""
        config = TaskConfigSchema(tasks=[])
        cmd = builder.build_command(config, ETL_PATH)
        assert "--tasks" not in cmd


# ---------------------------------------------------------------------------
# 7 种 Flow
# ---------------------------------------------------------------------------

class TestFlows:
    @pytest.mark.parametrize("flow_id", sorted(VALID_FLOWS))
    def test_all_flows_accepted(self, builder: CLIBuilder, flow_id: str):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"], flow=flow_id)
        cmd = builder.build_command(config, ETL_PATH)
        idx = cmd.index("--flow")
        assert cmd[idx + 1] == flow_id

    def test_default_flow_is_api_ods_dwd(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"])
        cmd = builder.build_command(config, ETL_PATH)
        idx = cmd.index("--flow")
        assert cmd[idx + 1] == "api_ods_dwd"


# ---------------------------------------------------------------------------
# 4 种处理模式
# ---------------------------------------------------------------------------

class TestProcessingModes:
    @pytest.mark.parametrize("mode", sorted(VALID_PROCESSING_MODES))
    def test_all_modes_accepted(self, builder: CLIBuilder, mode: str):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"], processing_mode=mode)
        cmd = builder.build_command(config, ETL_PATH)
        idx = cmd.index("--processing-mode")
        assert cmd[idx + 1] == mode

    def test_fetch_before_verify_only_in_verify_mode(self, builder: CLIBuilder):
        """--fetch-before-verify 仅在 verify_only 模式下生效"""
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            processing_mode="verify_only",
            fetch_before_verify=True,
        )
        cmd = builder.build_command(config, ETL_PATH)
        assert "--fetch-before-verify" in cmd

    def test_fetch_before_verify_ignored_in_increment_mode(self, builder: CLIBuilder):
        """increment_only 模式下 fetch_before_verify=True 不应生成参数"""
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            processing_mode="increment_only",
            fetch_before_verify=True,
        )
        cmd = builder.build_command(config, ETL_PATH)
        assert "--fetch-before-verify" not in cmd


# ---------------------------------------------------------------------------
# 时间窗口
# ---------------------------------------------------------------------------

class TestTimeWindow:
    def test_lookback_mode_generates_lookback_args(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            window_mode="lookback",
            lookback_hours=48,
            overlap_seconds=1200,
        )
        cmd = builder.build_command(config, ETL_PATH)
        idx_lb = cmd.index("--lookback-hours")
        assert cmd[idx_lb + 1] == "48"
        idx_ol = cmd.index("--overlap-seconds")
        assert cmd[idx_ol + 1] == "1200"
        # lookback 模式不应生成 --window-start / --window-end
        assert "--window-start" not in cmd
        assert "--window-end" not in cmd

    def test_custom_mode_generates_window_args(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            window_mode="custom",
            window_start="2026-01-01",
            window_end="2026-01-31",
        )
        cmd = builder.build_command(config, ETL_PATH)
        idx_s = cmd.index("--window-start")
        assert cmd[idx_s + 1] == "2026-01-01"
        idx_e = cmd.index("--window-end")
        assert cmd[idx_e + 1] == "2026-01-31"
        # custom 模式不应生成 --lookback-hours / --overlap-seconds
        assert "--lookback-hours" not in cmd
        assert "--overlap-seconds" not in cmd

    def test_window_split_with_days(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            window_split="day",
            window_split_days=10,
        )
        cmd = builder.build_command(config, ETL_PATH)
        idx = cmd.index("--window-split")
        assert cmd[idx + 1] == "day"
        idx_d = cmd.index("--window-split-days")
        assert cmd[idx_d + 1] == "10"

    def test_window_split_none_not_generated(self, builder: CLIBuilder):
        """window_split='none' 不应生成 --window-split 参数"""
        config = TaskConfigSchema(tasks=["ODS_MEMBER"], window_split="none")
        cmd = builder.build_command(config, ETL_PATH)
        assert "--window-split" not in cmd


# ---------------------------------------------------------------------------
# store_id 自动注入
# ---------------------------------------------------------------------------

class TestStoreId:
    def test_store_id_injected(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"], store_id=42)
        cmd = builder.build_command(config, ETL_PATH)
        idx = cmd.index("--store-id")
        assert cmd[idx + 1] == "42"

    def test_store_id_none_not_generated(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"], store_id=None)
        cmd = builder.build_command(config, ETL_PATH)
        assert "--store-id" not in cmd


# ---------------------------------------------------------------------------
# dry_run
# ---------------------------------------------------------------------------

class TestDryRun:
    def test_dry_run_flag(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"], dry_run=True)
        cmd = builder.build_command(config, ETL_PATH)
        assert "--dry-run" in cmd

    def test_no_dry_run_flag(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"], dry_run=False)
        cmd = builder.build_command(config, ETL_PATH)
        assert "--dry-run" not in cmd


# ---------------------------------------------------------------------------
# extra_args
# ---------------------------------------------------------------------------

class TestExtraArgs:
    def test_supported_value_arg(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            extra_args={"pg_dsn": "postgresql://localhost/test"},
        )
        cmd = builder.build_command(config, ETL_PATH)
        idx = cmd.index("--pg-dsn")
        assert cmd[idx + 1] == "postgresql://localhost/test"

    def test_supported_bool_arg(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            extra_args={"force_window_override": True},
        )
        cmd = builder.build_command(config, ETL_PATH)
        assert "--force-window-override" in cmd

    def test_unsupported_arg_ignored(self, builder: CLIBuilder):
        """不在 CLI_SUPPORTED_ARGS 中的键应被忽略"""
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            extra_args={"unknown_param": "value"},
        )
        cmd = builder.build_command(config, ETL_PATH)
        assert "--unknown-param" not in cmd

    def test_none_value_ignored(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            extra_args={"pg_dsn": None},
        )
        cmd = builder.build_command(config, ETL_PATH)
        assert "--pg-dsn" not in cmd

    def test_false_bool_arg_not_generated(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            extra_args={"force_window_override": False},
        )
        cmd = builder.build_command(config, ETL_PATH)
        assert "--force-window-override" not in cmd


# ---------------------------------------------------------------------------
# build_command_string
# ---------------------------------------------------------------------------

class TestBuildCommandString:
    def test_returns_string(self, builder: CLIBuilder):
        config = TaskConfigSchema(tasks=["ODS_MEMBER"])
        result = builder.build_command_string(config, ETL_PATH)
        assert isinstance(result, str)
        assert "python -m cli.main" in result

    def test_quotes_args_with_spaces(self, builder: CLIBuilder):
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            extra_args={"pg_dsn": "host=localhost dbname=test"},
        )
        result = builder.build_command_string(config, ETL_PATH)
        # 包含空格的值应被引号包裹
        assert '"host=localhost dbname=test"' in result
