import pandas as pd
import pytest

from xfintech.data.source.tushare.stock.companyoverview import CompanyOverview
from xfintech.data.source.tushare.stock.companyoverview.constant import (
    KEY,
    NAME,
    PAGINATE,
    SOURCE,
    TARGET,
)

# Test Fixtures


class FakeConnection:
    """Fake Tushare connection for testing"""

    def __init__(self, frame: pd.DataFrame):
        self.frame = frame

    def express_vip(self, **kwargs):
        """Mock express_vip API call"""
        return self.frame


class FakeSession:
    """Fake session for testing"""

    def __init__(self, connection: FakeConnection):
        self.connection = connection


@pytest.fixture
def mock_session():
    """Create a mock session with empty data"""
    fake_conn = FakeConnection(frame=pd.DataFrame())
    return FakeSession(fake_conn)


@pytest.fixture
def sample_source_data():
    """Sample company overview data in Tushare format"""
    return pd.DataFrame(
        {
            "ts_code": ["000001.SZ", "000001.SZ", "000002.SZ"],
            "ann_date": ["20200430", "20201031", "20200430"],
            "end_date": ["20200331", "20200930", "20200331"],
            "revenue": ["100000000.50", "120000000.75", "50000000.25"],
            "operate_profit": ["15000000.30", "18000000.40", "6000000.15"],
            "total_profit": ["16000000.20", "19000000.35", "7000000.10"],
            "n_income": ["12000000.00", "14000000.00", "5000000.00"],
            "total_assets": ["500000000.50", "550000000.75", "200000000.25"],
            "total_hldr_eqy_exc_min_int": ["200000000.50", "220000000.75", "80000000.25"],
            "diluted_eps": ["0.50", "0.60", "0.25"],
            "diluted_roe": ["8.5", "9.2", "7.3"],
            "yoy_net_profit": ["10000000.00", "12000000.00", "4500000.00"],
            "bps": ["5.5", "6.0", "4.2"],
            "yoy_sales": ["15.5", "18.2", "12.3"],
            "yoy_op": ["20.3", "22.5", "15.8"],
            "yoy_tp": ["18.7", "20.9", "14.2"],
            "yoy_dedu_np": ["16.5", "18.8", "13.5"],
            "yoy_eps": ["15.0", "17.5", "12.0"],
            "yoy_roe": ["1.5", "2.0", "1.0"],
            "growth_assets": ["10.5", "12.3", "8.7"],
            "yoy_equity": ["11.2", "13.5", "9.3"],
            "growth_bps": ["12.0", "14.2", "10.1"],
            "or_last_year": ["85000000.00", "100000000.00", "44000000.00"],
            "op_last_year": ["12500000.00", "15000000.00", "5200000.00"],
            "tp_last_year": ["13500000.00", "16000000.00", "6100000.00"],
            "np_last_year": ["10300000.00", "12000000.00", "4400000.00"],
            "eps_last_year": ["0.43", "0.51", "0.22"],
            "open_net_assets": ["180000000.00", "200000000.00", "73000000.00"],
            "open_bps": ["4.9", "5.4", "3.8"],
            "perf_summary": ["业绩稳定增长", "持续盈利", "良好表现"],
            "is_audit": ["1", "0", "1"],
            "remark": ["", "预计数据", ""],
        }
    )


# Initialization Tests


def test_companyoverview_init_basic(mock_session):
    """Test basic initialization with required session"""
    overview = CompanyOverview(session=mock_session)
    assert overview.name == NAME
    assert overview.key == KEY
    assert overview.source == SOURCE
    assert overview.target == TARGET
    assert overview.paginate == PAGINATE


def test_companyoverview_init_with_params_dict(mock_session):
    """Test initialization with params as dict"""
    params = {"ts_code": "000001.SZ", "start_date": "20200101", "end_date": "20201231"}
    overview = CompanyOverview(session=mock_session, params=params)
    assert overview.params.get("ts_code") == "000001.SZ"
    assert overview.params.get("start_date") == "20200101"
    assert overview.params.get("end_date") == "20201231"


def test_companyoverview_init_with_params_object(mock_session):
    """Test initialization with params as Params object"""
    from xfintech.data.common.params import Params

    params = Params(ts_code="000001.SZ", start_date="20200101")
    overview = CompanyOverview(session=mock_session, params=params)
    assert overview.params.get("ts_code") == "000001.SZ"


def test_companyoverview_init_with_cache_bool_true(mock_session):
    """Test initialization with cache enabled"""
    overview = CompanyOverview(session=mock_session, cache=True)
    assert overview.cache is not None


def test_companyoverview_init_with_cache_bool_false(mock_session):
    """Test initialization with cache disabled"""
    overview = CompanyOverview(session=mock_session, cache=False)
    assert overview.cache is None


def test_companyoverview_init_with_cache_dict(mock_session):
    """Test initialization with cache config dict"""
    cache_config = {"path": "/tmp/cache", "key": "test"}
    overview = CompanyOverview(session=mock_session, cache=cache_config)
    assert overview.cache is not None


def test_companyoverview_init_default_paginate_limit(mock_session):
    """Test that default pagination limit is set correctly"""
    overview = CompanyOverview(session=mock_session)
    assert overview.paginate.pagesize == 1000
    assert overview.paginate.pagelimit == 1000


def test_companyoverview_init_with_all_params(mock_session):
    """Test initialization with all parameters"""
    params = {"ts_code": "000001.SZ", "start_date": "20200101", "end_date": "20201231"}
    coolant = {"interval": 0.5}
    retry = {"max_attempts": 3}
    cache = True

    overview = CompanyOverview(session=mock_session, params=params, coolant=coolant, retry=retry, cache=cache)
    assert overview.params.get("ts_code") == "000001.SZ"
    assert overview.cache is not None


def test_companyoverview_constants():
    """Test that constants are correctly defined"""
    assert NAME == "companyoverview"
    assert KEY == "/tushare/companyoverview"
    assert SOURCE.name == "express_vip"
    assert TARGET.name == "companyoverview"
    assert PAGINATE.pagesize == 1000


# Transform Tests


def test_companyoverview_transform_basic(sample_source_data):
    """Test basic transformation of overview data"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    assert len(result) == 3
    assert "code" in result.columns
    assert "date" in result.columns
    assert "datecode" in result.columns


def test_companyoverview_transform_code_mapping(sample_source_data):
    """Test that ts_code is correctly mapped to code"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    assert result.iloc[0]["code"] == "000001.SZ"
    assert result.iloc[2]["code"] == "000002.SZ"


def test_companyoverview_transform_date_format(sample_source_data):
    """Test that dates are properly formatted"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    assert pd.api.types.is_datetime64_any_dtype(result["date"]) is False
    assert result.iloc[0]["date"] == "2020-03-31"


def test_companyoverview_transform_datecode_preserved(sample_source_data):
    """Test that datecode is correctly created from end_date"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    assert result.iloc[0]["datecode"] == "20200331"
    assert result.iloc[1]["datecode"] == "20200930"


def test_companyoverview_transform_numeric_conversions(sample_source_data):
    """Test that numeric fields are properly converted"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    assert result.iloc[0]["revenue"] == 100000000.50
    assert result.iloc[0]["operate_profit"] == 15000000.30
    assert pd.api.types.is_numeric_dtype(result["revenue"])


def test_companyoverview_transform_string_fields(sample_source_data):
    """Test that string fields are properly converted"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    assert result.iloc[0]["perf_summary"] == "业绩稳定增长"
    assert result.iloc[1]["remark"] == "预计数据"


def test_companyoverview_transform_empty_dataframe():
    """Test transformation with empty dataframe"""
    fake_conn = FakeConnection(frame=pd.DataFrame())
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(pd.DataFrame())

    assert isinstance(result, pd.DataFrame)
    assert result.empty
    assert len(result.columns) == len(TARGET.list_column_names())


def test_companyoverview_transform_none_input():
    """Test transformation with None input"""
    fake_conn = FakeConnection(frame=pd.DataFrame())
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(None)

    assert isinstance(result, pd.DataFrame)
    assert result.empty


def test_companyoverview_transform_handles_invalid_dates():
    """Test that invalid dates are handled gracefully"""
    data = pd.DataFrame(
        {
            "ts_code": ["000001.SZ"],
            "ann_date": ["invalid"],
            "end_date": ["20200331"],
            "revenue": ["100000000.00"],
            "is_audit": ["1"],
        }
    )

    fake_conn = FakeConnection(frame=data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(data)

    assert len(result) == 1
    assert pd.isna(result.iloc[0]["ann_date"])


def test_companyoverview_transform_removes_duplicates(sample_source_data):
    """Test that duplicate rows are removed"""
    # Create data with duplicates
    data = pd.concat([sample_source_data, sample_source_data.iloc[[0]]], ignore_index=True)

    fake_conn = FakeConnection(frame=data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(data)

    # Should have 3 unique rows, not 4
    assert len(result) == 3


def test_companyoverview_transform_sorts_by_code_and_date(sample_source_data):
    """Test that output is sorted by code and date"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    # Check first row is 000001.SZ, 2020-03-31
    assert result.iloc[0]["code"] == "000001.SZ"
    assert result.iloc[0]["date"] == "2020-03-31"
    # Check last row is 000002.SZ
    assert result.iloc[2]["code"] == "000002.SZ"


def test_companyoverview_transform_resets_index():
    """Test that index is reset after transformation"""
    data = pd.DataFrame(
        {
            "ts_code": ["000001.SZ", "000002.SZ"],
            "ann_date": ["20200430", "20200430"],
            "end_date": ["20200331", "20200331"],
            "revenue": ["100000000.00", "50000000.00"],
            "is_audit": ["1", "1"],
        },
        index=[5, 10],
    )

    fake_conn = FakeConnection(frame=data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(data)

    assert result.index.tolist() == [0, 1]


def test_companyoverview_transform_only_target_columns(sample_source_data):
    """Test that only target columns are in output"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.transform(sample_source_data)

    expected_cols = set(TARGET.list_column_names())
    actual_cols = set(result.columns)

    assert actual_cols == expected_cols


# Run Method Tests


def test_companyoverview_run_with_cache_hit(sample_source_data):
    """Test that run() returns cached data when available"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"}, cache=True)

    # First run should fetch and cache
    result1 = overview.run()

    # Second run should return cached data
    result2 = overview.run()

    pd.testing.assert_frame_equal(result1, result2)


def test_companyoverview_run_basic_date_range(sample_source_data):
    """Test run() with date range parameters"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(
        session=session,
        params={"ts_code": "000001.SZ", "start_date": "20200101", "end_date": "20201231"},
    )

    result = overview.run()

    assert isinstance(result, pd.DataFrame)
    assert len(result) > 0


def test_companyoverview_run_with_period_param(sample_source_data):
    """Test run() with period parameter"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(
        session=session,
        params={"ts_code": "000001.SZ", "period": "2020-3"},
    )
    result = overview.run()
    assert isinstance(result, pd.DataFrame)


def test_companyoverview_run_with_datetime_params(sample_source_data):
    """Test run() with datetime objects as parameters"""
    from datetime import datetime

    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(
        session=session,
        params={
            "ts_code": "000001.SZ",
            "start_date": datetime(2020, 1, 1),
            "end_date": datetime(2020, 12, 31),
        },
    )

    result = overview.run()

    assert isinstance(result, pd.DataFrame)


def test_companyoverview_run_adds_fields_param(sample_source_data):
    """Test that run() adds fields parameter if not provided"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    # Monkey patch _fetchall to capture params
    overview._fetchall
    captured_params = {}

    def mock_fetchall(api, **kwargs):
        captured_params.update(kwargs)
        return sample_source_data

    overview._fetchall = mock_fetchall
    overview.run()

    assert "fields" in captured_params
    assert len(captured_params["fields"]) > 0


def test_companyoverview_run_sets_cache(sample_source_data):
    """Test that run() saves result to cache"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"}, cache=True)

    result = overview.run()

    # Cache should be set after run
    cached = overview._load_cache()
    assert cached is not None
    pd.testing.assert_frame_equal(result, cached)


def test_companyoverview_run_calls_transform(sample_source_data):
    """Test that run() calls transform method"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    # Monkey patch transform to track if it was called
    original_transform = overview.transform
    transform_called = {"called": False}

    def mock_transform(data):
        transform_called["called"] = True
        return original_transform(data)

    overview.transform = mock_transform
    overview.run()

    assert transform_called["called"] is True


def test_companyoverview_run_uses_express_vip_api(sample_source_data):
    """Test that run() uses express_vip API"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    # Monkey patch _fetchall to capture API
    captured_api = {"api": None}

    def mock_fetchall(api, **kwargs):
        captured_api["api"] = api
        return sample_source_data

    overview._fetchall = mock_fetchall
    overview.run()

    assert captured_api["api"] == session.connection.express_vip


# Integration Tests


def test_companyoverview_full_workflow(sample_source_data):
    """Test complete workflow from initialization to output"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)

    overview = CompanyOverview(
        session=session,
        params={"ts_code": "000001.SZ", "start_date": "20200101", "end_date": "20201231"},
        cache=True,
    )

    result = overview.run()

    # Verify output structure
    assert isinstance(result, pd.DataFrame)
    assert len(result) == 3
    assert "code" in result.columns
    assert "date" in result.columns
    assert "revenue" in result.columns
    assert "n_income" in result.columns

    # Verify data types
    assert pd.api.types.is_numeric_dtype(result["revenue"])
    assert result.iloc[0]["code"] == "000001.SZ"


def test_companyoverview_cache_persistence(sample_source_data):
    """Test that cache persists across multiple runs"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"}, cache=True)

    # Run once to populate cache
    result1 = overview.run()

    # Clear in-memory data to simulate fresh load
    overview2 = CompanyOverview(session=session, params={"ts_code": "000001.SZ"}, cache=True)
    result2 = overview2.run()

    pd.testing.assert_frame_equal(result1, result2)


def test_companyoverview_params_uniqueness(mock_session):
    """Test that different params produce different cache keys"""
    overview1 = CompanyOverview(
        session=mock_session,
        params={"ts_code": "000001.SZ", "start_date": "20200101"},
        cache=True,
    )
    overview2 = CompanyOverview(
        session=mock_session,
        params={"ts_code": "000001.SZ", "start_date": "20210101"},
        cache=True,
    )

    # Test that different params are properly stored
    assert overview1.params.get("start_date") == "20200101"
    assert overview2.params.get("start_date") == "20210101"
    assert overview1.params.get("start_date") != overview2.params.get("start_date")


def test_companyoverview_different_stocks(sample_source_data):
    """Test that different stock codes are handled correctly"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)

    overview1 = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})
    result1 = overview1.run()

    overview2 = CompanyOverview(session=session, params={"ts_code": "000002.SZ"})
    result2 = overview2.run()

    # Both should have data for their respective stocks
    assert "000001.SZ" in result1["code"].values
    assert "000002.SZ" in result2["code"].values


def test_companyoverview_empty_result_handling():
    """Test handling of empty API results"""
    empty_data = pd.DataFrame()
    fake_conn = FakeConnection(frame=empty_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.run()

    assert isinstance(result, pd.DataFrame)
    assert result.empty
    assert len(result.columns) > 0


def test_companyoverview_large_dataset_handling():
    """Test handling of large datasets"""
    # Create large dataset
    large_data = pd.DataFrame(
        {
            "ts_code": ["000001.SZ"] * 1000,
            "ann_date": ["20200430"] * 1000,
            "end_date": ["20200331"] * 1000,
            "revenue": ["100000000.00"] * 1000,
            "is_audit": ["1"] * 1000,
        }
    )

    fake_conn = FakeConnection(frame=large_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.run()

    # Should deduplicate to 1 row
    assert len(result) == 1


def test_companyoverview_without_cache(sample_source_data):
    """Test that class works correctly without caching"""
    fake_conn = FakeConnection(frame=sample_source_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"}, cache=False)

    result = overview.run()

    assert isinstance(result, pd.DataFrame)
    assert len(result) > 0
    assert overview.cache is None


def test_companyoverview_handles_missing_numeric_fields():
    """Test that missing numeric fields don't break transformation"""
    # Data with only subset of fields
    minimal_data = pd.DataFrame(
        {
            "ts_code": ["000001.SZ"],
            "ann_date": ["20200430"],
            "end_date": ["20200331"],
            "revenue": ["100000000.00"],
            "is_audit": ["1"],
        }
    )

    fake_conn = FakeConnection(frame=minimal_data)
    session = FakeSession(fake_conn)
    overview = CompanyOverview(session=session, params={"ts_code": "000001.SZ"})

    result = overview.run()

    # Should complete without error
    assert len(result) == 1
    # Missing fields should be NaN
    assert pd.isna(result.iloc[0]["operate_profit"])


def test_companyoverview_api_doc_reference():
    """Test that class correctly references API documentation"""
    assert "doc_id=46" in CompanyOverview.__doc__
