"""
Unit tests for AKShare CLI harness core modules.

Uses synthetic data and mocks — no network calls required.
"""

import json
import os
import tempfile

import pandas as pd
import pytest

from akshare_cli.core.registry import (
    DISABLED_FUNCTIONS,
    _auto_fill_date_params,
    call_function,
    get_all_functions,
    get_domains,
    get_function,
    get_function_info,
    list_functions_by_domain,
    parse_param_value,
    search_functions,
)
from akshare_cli.core.session import Session
from akshare_cli.core.export import (
    auto_export,
    export_csv,
    export_json,
    export_excel,
    export_markdown,
    format_csv,
    format_json,
    format_table,
)
from akshare_cli.utils.formatting import (
    display_result,
    truncate_string,
)


# ─── Fixtures ────────────────────────────────────────────────────────────────

@pytest.fixture
def sample_df():
    """Create a sample DataFrame for testing."""
    return pd.DataFrame({
        "date": ["2024-01-01", "2024-01-02", "2024-01-03"],
        "open": [10.0, 10.5, 11.0],
        "close": [10.5, 11.0, 10.8],
        "volume": [1000, 1500, 1200],
    })


@pytest.fixture
def large_df():
    """Create a large DataFrame for truncation tests."""
    return pd.DataFrame({
        "idx": list(range(200)),
        "value": [float(i) * 1.1 for i in range(200)],
    })


@pytest.fixture
def tmp_dir():
    """Create a temporary directory for export tests."""
    with tempfile.TemporaryDirectory() as d:
        yield d


@pytest.fixture
def session():
    """Create a fresh Session instance."""
    with tempfile.TemporaryDirectory() as d:
        yield Session(session_dir=d)


# ─── Registry Tests ─────────────────────────────────────────────────────────


class TestRegistry:
    def test_get_all_functions(self):
        funcs = get_all_functions()
        assert isinstance(funcs, dict)
        assert len(funcs) > 500, f"Expected 500+ functions, got {len(funcs)}"

    def test_get_function_existing(self):
        func = get_function("stock_zh_a_hist")
        assert func is not None
        assert callable(func)

    def test_get_function_missing(self):
        func = get_function("totally_nonexistent_function_xyz")
        assert func is None

    def test_search_functions(self):
        results = search_functions("stock_zh")
        assert len(results) > 0
        assert all("stock_zh" in name.lower() for name in results)

    def test_search_functions_case_insensitive(self):
        results_lower = search_functions("stock_zh")
        results_upper = search_functions("STOCK_ZH")
        assert results_lower == results_upper

    def test_search_functions_no_match(self):
        results = search_functions("zzzznonexistent999")
        assert results == []

    def test_list_functions_by_domain(self):
        results = list_functions_by_domain("stock")
        assert len(results) > 0
        assert all(name.startswith("stock_") for name in results)

    def test_list_functions_by_domain_empty(self):
        results = list_functions_by_domain("zzzznonexistent")
        assert results == []

    def test_get_function_info(self):
        info = get_function_info("stock_zh_a_hist")
        assert info is not None
        assert info["name"] == "stock_zh_a_hist"
        assert isinstance(info["params"], list)
        assert len(info["params"]) > 0
        assert isinstance(info["docstring"], str)

        # Check known parameters
        param_names = [p["name"] for p in info["params"]]
        assert "symbol" in param_names
        assert "period" in param_names

    def test_get_function_info_missing(self):
        info = get_function_info("totally_nonexistent_xyz")
        assert info is None

    def test_get_domains(self):
        domains = get_domains()
        assert isinstance(domains, list)
        assert len(domains) > 10
        assert "stock" in domains
        assert "fund" in domains
        assert "futures" in domains

    def test_parse_param_value_string(self):
        assert parse_param_value("hello", {}) == "hello"

    def test_parse_param_value_int(self):
        assert parse_param_value("42", {}) == 42

    def test_parse_param_value_float(self):
        assert parse_param_value("3.14", {}) == 3.14

    def test_parse_param_value_bool_true(self):
        assert parse_param_value("true", {}) is True
        assert parse_param_value("True", {}) is True
        assert parse_param_value("yes", {}) is True

    def test_parse_param_value_bool_false(self):
        assert parse_param_value("false", {}) is False
        assert parse_param_value("False", {}) is False
        assert parse_param_value("no", {}) is False

    def test_parse_param_value_none(self):
        assert parse_param_value("none", {}) is None
        assert parse_param_value("None", {}) is None
        assert parse_param_value("null", {}) is None

    def test_call_function_not_found(self):
        with pytest.raises(ValueError, match="not found"):
            call_function("totally_nonexistent_function_xyz", {})

    def test_disabled_functions_excluded(self):
        """All DISABLED_FUNCTIONS should be absent from get_all_functions()."""
        all_funcs = get_all_functions()
        for name in DISABLED_FUNCTIONS:
            assert name not in all_funcs, f"{name} is disabled but still in get_all_functions()"

    def test_disabled_functions_minimal(self):
        """Only permanently removed functions should be in DISABLED_FUNCTIONS."""
        assert len(DISABLED_FUNCTIONS) <= 5, (
            "DISABLED_FUNCTIONS should be minimal — use runtime fallback instead"
        )


# ─── Session Tests ───────────────────────────────────────────────────────────


class TestSession:
    def test_session_init(self, session):
        assert session.last_result is None
        assert session.last_function is None
        assert session.history == []
        assert isinstance(session.preferences, dict)

    def test_record_call(self, session, sample_df):
        session.record_call("test_func", {"key": "val"}, sample_df)
        assert session.last_function == "test_func"
        assert session.last_result is not None
        assert len(session.history) == 1
        assert session.history[0]["function"] == "test_func"
        assert session.history[0]["rows"] == 3

    def test_last_result(self, session, sample_df):
        session.record_call("func1", {}, sample_df)
        assert session.last_result.equals(sample_df)

    def test_history_ordering(self, session, sample_df):
        session.record_call("func1", {}, sample_df)
        session.record_call("func2", {}, sample_df)
        session.record_call("func3", {}, sample_df)
        assert len(session.history) == 3
        assert session.history[0]["function"] == "func1"
        assert session.history[2]["function"] == "func3"

    def test_preferences_get_set(self, session):
        session.set_preference("max_rows", 100)
        assert session.get_preference("max_rows") == 100

    def test_preferences_default(self, session):
        assert session.get_preference("nonexistent", "default") == "default"

    def test_save_history(self, session, sample_df, tmp_dir):
        session.record_call("test_func", {"a": "b"}, sample_df)
        filepath = os.path.join(tmp_dir, "test_history.json")
        saved = session.save_history(filepath)
        assert os.path.exists(saved)

        with open(saved, encoding="utf-8") as f:
            data = json.load(f)
        assert "history" in data
        assert len(data["history"]) == 1
        assert data["history"][0]["function"] == "test_func"

    def test_clear(self, session, sample_df):
        session.record_call("func1", {}, sample_df)
        session.clear()
        assert session.last_result is None
        assert session.last_function is None
        assert session.history == []

    def test_summary(self, session, sample_df):
        session.record_call("func1", {}, sample_df)
        s = session.summary()
        assert s["total_calls"] == 1
        assert s["last_function"] == "func1"
        assert "3x4" in s["last_result_shape"]


# ─── Export Tests ────────────────────────────────────────────────────────────


class TestExport:
    def test_export_csv(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "test.csv")
        result = export_csv(sample_df, path)
        assert os.path.exists(result)
        loaded = pd.read_csv(result)
        assert len(loaded) == 3
        assert list(loaded.columns) == ["date", "open", "close", "volume"]

    def test_export_json(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "test.json")
        result = export_json(sample_df, path)
        assert os.path.exists(result)
        with open(result, encoding="utf-8") as f:
            data = json.load(f)
        assert isinstance(data, list)
        assert len(data) == 3

    def test_export_excel(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "test.xlsx")
        result = export_excel(sample_df, path)
        assert os.path.exists(result)
        loaded = pd.read_excel(result)
        assert len(loaded) == 3

    def test_export_markdown(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "test.md")
        result = export_markdown(sample_df, path)
        assert os.path.exists(result)
        with open(result, encoding="utf-8") as f:
            content = f.read()
        assert "date" in content
        assert "|" in content

    def test_format_table(self, sample_df):
        table = format_table(sample_df)
        assert "date" in table
        assert "2024-01-01" in table
        assert "10.0" in table

    def test_format_table_truncated(self, large_df):
        table = format_table(large_df, max_rows=20)
        assert "200 rows total" in table

    def test_format_json(self, sample_df):
        result = format_json(sample_df)
        data = json.loads(result)
        assert data["total_rows"] == 3
        assert "columns" in data
        assert "data" in data

    def test_format_json_limited(self, large_df):
        result = format_json(large_df, max_rows=5)
        data = json.loads(result)
        assert len(data["data"]) == 5

    def test_format_csv(self, sample_df):
        result = format_csv(sample_df)
        lines = result.strip().split("\n")
        assert len(lines) == 4  # header + 3 rows
        assert "date" in lines[0]

    def test_auto_export_csv(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "auto.csv")
        result = auto_export(sample_df, path)
        assert os.path.exists(result)

    def test_auto_export_json(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "auto.json")
        result = auto_export(sample_df, path)
        assert os.path.exists(result)

    def test_auto_export_excel(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "auto.xlsx")
        result = auto_export(sample_df, path)
        assert os.path.exists(result)

    def test_auto_export_unsupported(self, sample_df, tmp_dir):
        path = os.path.join(tmp_dir, "auto.xyz")
        with pytest.raises(ValueError, match="Unsupported"):
            auto_export(sample_df, path)


# ─── Formatting Tests ────────────────────────────────────────────────────────


class TestFormatting:
    def test_display_result_table(self, sample_df):
        output = display_result(sample_df, output_format="table")
        assert "date" in output
        assert "2024-01-01" in output

    def test_display_result_json(self, sample_df):
        output = display_result(sample_df, output_format="json")
        data = json.loads(output)
        assert data["total_rows"] == 3

    def test_display_result_csv(self, sample_df):
        output = display_result(sample_df, output_format="csv")
        assert "date,open,close,volume" in output

    def test_display_result_with_limit(self, large_df):
        output = display_result(large_df, output_format="table", max_rows=10)
        assert "200 rows total" in output

    def test_truncate_string_short(self):
        assert truncate_string("hello", 10) == "hello"

    def test_truncate_string_long(self):
        result = truncate_string("a" * 100, 20)
        assert len(result) == 20
        assert result.endswith("...")

    def test_truncate_string_exact(self):
        assert truncate_string("hello", 5) == "hello"


# ─── Auto-fill Date Tests ────────────────────────────────────────────────────


class TestAutoFillDate:
    """Tests for _auto_fill_date_params."""

    def _make_param(self, name, default=None, has_default=True):
        p = {"name": name, "has_default": has_default}
        if has_default and default is not None:
            p["default"] = default
        return p

    def test_fills_date_param_with_today(self):
        from datetime import date
        params = [self._make_param("date", "20251126")]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        expected = date.today().strftime("%Y%m%d")
        assert kwargs["date"] == expected

    def test_fills_start_date_param(self):
        from datetime import date
        params = [self._make_param("start_date", "20240101")]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        assert kwargs["start_date"] == date.today().strftime("%Y%m%d")

    def test_fills_end_date_param(self):
        from datetime import date
        params = [self._make_param("end_date", "20240301")]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        assert kwargs["end_date"] == date.today().strftime("%Y%m%d")

    def test_skips_user_provided_date(self):
        params = [self._make_param("date", "20251126")]
        kwargs = {"date": "20260101"}
        _auto_fill_date_params(params, kwargs)
        assert kwargs["date"] == "20260101"  # unchanged

    def test_skips_non_date_param(self):
        params = [self._make_param("symbol", "000001")]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        assert "symbol" not in kwargs

    def test_skips_non_yyyymmdd_default(self):
        params = [self._make_param("date", "2024-01-01")]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        assert "date" not in kwargs

    def test_skips_no_default(self):
        params = [{"name": "date", "has_default": False}]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        assert "date" not in kwargs

    def test_skips_none_default(self):
        params = [self._make_param("date", None)]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        assert "date" not in kwargs

    def test_skips_int_default(self):
        params = [self._make_param("date", 20240101)]
        kwargs = {}
        _auto_fill_date_params(params, kwargs)
        assert "date" not in kwargs

    def test_multiple_date_params(self):
        from datetime import date
        params = [
            self._make_param("start_date", "20240101"),
            self._make_param("end_date", "20240301"),
            self._make_param("symbol", "000001"),
        ]
        kwargs = {"start_date": "20260101"}  # user provided start_date only
        _auto_fill_date_params(params, kwargs)
        today = date.today().strftime("%Y%m%d")
        assert kwargs["start_date"] == "20260101"  # user value kept
        assert kwargs["end_date"] == today  # auto-filled
        assert "symbol" not in kwargs  # not a date param
