import pytest
import pandas as pd
from ontario_data.cache import CacheManager, InvalidQueryError
import duckdb


@pytest.fixture
def cache(tmp_path):
    db_path = str(tmp_path / "test.duckdb")
    mgr = CacheManager(db_path)
    mgr.initialize()
    return mgr


class TestInitialization:
    def test_creates_metadata_tables(self, cache):
        tables = cache.execute_sql(
            "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
        )
        table_names = [t[0] for t in tables]
        assert "_cache_metadata" in table_names
        assert "_dataset_metadata" in table_names

    def test_idempotent_init(self, cache):
        cache.initialize()  # Should not raise
        tables = cache.execute_sql(
            "SELECT count(*) FROM information_schema.tables WHERE table_name='_cache_metadata'"
        )
        assert tables[0][0] == 1


class TestStoreDataFrame:
    def test_store_and_retrieve(self, cache):
        df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]})
        cache.store_resource("r1", "ds1", "test_table", df, "http://example.com/data.csv")

        result = cache.query("SELECT * FROM test_table ORDER BY name")
        assert len(result) == 2
        assert [r["name"] for r in result] == ["Alice", "Bob"]

    def test_metadata_recorded(self, cache):
        df = pd.DataFrame({"x": [1, 2, 3]})
        cache.store_resource("r1", "ds1", "tbl", df, "http://example.com")

        meta = cache.get_resource_meta("r1")
        assert meta is not None
        assert meta["row_count"] == 3

    def test_force_refresh_replaces(self, cache):
        df1 = pd.DataFrame({"x": [1]})
        df2 = pd.DataFrame({"x": [1, 2, 3]})
        cache.store_resource("r1", "ds1", "tbl", df1, "http://example.com")
        cache.store_resource("r1", "ds1", "tbl", df2, "http://example.com")

        result = cache.execute_sql("SELECT count(*) FROM tbl")
        assert result[0][0] == 3

    def test_strips_whitespace_from_column_names(self, cache):
        """Columns with leading/trailing spaces are normalized at storage time."""
        df = pd.DataFrame([{"  Count  ": 1, " Name ": "Alice"}])
        cache.store_resource("res_ws1", "ds1", "test_ws", df, "http://example.com")
        rows = cache.query("SELECT * FROM test_ws")
        assert rows[0] == {"Count": 1, "Name": "Alice"}

    def test_strips_whitespace_does_not_alter_internal_content(self, cache):
        """Whitespace inside column values is left unchanged."""
        df = pd.DataFrame([{"Name": "  Alice  "}])
        cache.store_resource("res_ws2", "ds1", "test_ws2", df, "http://example.com")
        rows = cache.query("SELECT Name FROM test_ws2")
        assert rows[0]["Name"] == "  Alice  "

    def test_detects_scientific_notation_numerics(self, cache):
        """Columns with scientific notation strings are cast to DOUBLE."""
        df = pd.DataFrame([
            {"measurement": "1.23e-5", "label": "trace"},
            {"measurement": "4.56E+10", "label": "large"},
            {"measurement": "7.89e0", "label": "unit"},
        ])
        cache.store_resource("res_sci", "ds1", "test_sci", df, "http://example.com")
        rows = cache.query('SELECT measurement FROM test_sci WHERE measurement < 0.001')
        assert len(rows) == 1
        assert abs(rows[0]["measurement"] - 1.23e-5) < 1e-10


class TestCacheQueries:
    def test_list_cached(self, cache):
        df = pd.DataFrame({"x": [1]})
        cache.store_resource("r1", "ds1", "tbl1", df, "http://example.com/1")
        cache.store_resource("r2", "ds2", "tbl2", df, "http://example.com/2")

        cached = cache.list_cached()
        assert len(cached) == 2

    def test_is_cached(self, cache):
        assert not cache.is_cached("r1")
        df = pd.DataFrame({"x": [1]})
        cache.store_resource("r1", "ds1", "tbl", df, "http://example.com")
        assert cache.is_cached("r1")

    def test_get_table_name(self, cache):
        df = pd.DataFrame({"x": [1]})
        cache.store_resource("r1", "ds1", "my_table", df, "http://example.com")
        assert cache.get_table_name("r1") == "my_table"

    def test_remove_resource(self, cache):
        df = pd.DataFrame({"x": [1]})
        cache.store_resource("r1", "ds1", "tbl", df, "http://example.com")
        cache.remove_resource("r1")
        assert not cache.is_cached("r1")

    def test_cache_stats(self, cache):
        df = pd.DataFrame({"x": range(100)})
        cache.store_resource("r1", "ds1", "tbl", df, "http://example.com")
        stats = cache.get_stats()
        assert stats["table_count"] == 1
        assert stats["total_rows"] == 100


class TestSQLQuery:
    def test_run_sql(self, cache):
        df = pd.DataFrame({"name": ["Alice", "Bob"], "score": [90, 85]})
        cache.store_resource("r1", "ds1", "scores", df, "http://example.com")

        result = cache.query("SELECT name, score FROM scores WHERE score > 87")
        assert len(result) == 1
        assert result[0]["name"] == "Alice"


class TestSemicolonIntegration:
    """Integration tests for semicolon handling through cache.query().
    Pure unit tests for _has_semicolons_outside_strings live in test_sql_safety.py.
    """

    def test_query_cached_allows_semicolons_in_strings(self, cache):
        df = pd.DataFrame({"name": ["Phosphorus; total", "Nitrogen"], "value": [1.0, 2.0]})
        cache.store_resource("r1", "ds1", "params", df, "http://example.com")
        result = cache.query("SELECT * FROM params WHERE name = 'Phosphorus; total'")
        assert len(result) == 1
        assert result[0]["name"] == "Phosphorus; total"

    def test_query_cached_rejects_bare_semicolons(self, cache):
        df = pd.DataFrame({"x": [1]})
        cache.store_resource("r1", "ds1", "tbl", df, "http://example.com")
        with pytest.raises(InvalidQueryError):
            cache.query("SELECT * FROM tbl; DROP TABLE tbl")


class TestEmptyDataFrame:
    def test_store_zero_row_dataframe(self, cache):
        """0-row DataFrames are cached without error; metadata records row_count=0."""
        df = pd.DataFrame({"name": pd.Series([], dtype=str), "value": pd.Series([], dtype=float)})
        cache.store_resource("r_empty", "ds1", "empty_tbl", df, "http://example.com/empty.csv")

        assert cache.is_cached("r_empty")
        meta = cache.get_resource_meta("r_empty")
        assert meta["row_count"] == 0

        rows = cache.query("SELECT * FROM empty_tbl")
        assert rows == []

    def test_store_no_columns_raises(self, cache):
        """DataFrames with no columns are rejected — they produce unusable tables."""
        df = pd.DataFrame()
        with pytest.raises(ValueError, match="no columns"):
            cache.store_resource("r_nocols", "ds1", "nocols_tbl", df, "http://example.com")

    def test_transaction_rolls_back_on_duplicate_table(self, cache):
        """If CREATE TABLE fails (duplicate name without a prior cache entry),
        the transaction rolls back and no partial metadata row is left."""
        df = pd.DataFrame({"x": [1, 2]})
        # Manually create a table with the target name so CREATE TABLE will conflict.
        cache.execute_sql('CREATE TABLE orphan_tbl (x INTEGER)')

        with pytest.raises(Exception):
            cache.store_resource("r_dup", "ds1", "orphan_tbl", df, "http://example.com")

        # Metadata must NOT have been written.
        assert not cache.is_cached("r_dup")


class TestDatasetMetadata:
    def test_store_and_get_metadata(self, cache):
        meta = {"id": "ds1", "title": "Test", "organization": {"name": "health"}}
        cache.store_dataset_metadata("ds1", meta)
        result = cache.get_dataset_metadata("ds1")
        assert result["title"] == "Test"
