import pytest

from iterable.codecs import BZIP2Codec, GZIPCodec, LZMACodec

# Optional codecs - may not be available
try:
    from iterable.codecs import BrotliCodec
  # noqa: F401
except (ImportError, NameError):
    BrotliCodec = None

try:
    from iterable.codecs import ZSTDCodec
  # noqa: F401
except (ImportError, NameError):
    ZSTDCodec = None
from iterable.datatypes import (
    CSVIterable,
    JSONIterable,
    JSONLinesIterable,
    PickleIterable,
)

# Optional datatypes - may not be available
try:
    from iterable.datatypes import AVROIterable
  # noqa: F401
except (ImportError, NameError):
    AVROIterable = None

try:
    from iterable.datatypes import BSONIterable
  # noqa: F401
except (ImportError, NameError):
    BSONIterable = None

try:
    from iterable.datatypes import DBFIterable
  # noqa: F401
except (ImportError, NameError):
    DBFIterable = None

try:
    from iterable.datatypes import DuckDBIterable
  # noqa: F401
except (ImportError, NameError):
    DuckDBIterable = None

try:
    from iterable.datatypes import ORCIterable
  # noqa: F401
except (ImportError, NameError):
    ORCIterable = None

try:
    from iterable.datatypes import ParquetIterable
  # noqa: F401
except (ImportError, NameError):
    ParquetIterable = None

try:
    from iterable.datatypes import XLSIterable
  # noqa: F401
except (ImportError, NameError):
    XLSIterable = None

try:
    from iterable.datatypes import XLSXIterable
  # noqa: F401
except (ImportError, NameError):
    XLSXIterable = None

try:
    from iterable.datatypes import XMLIterable
  # noqa: F401
except (ImportError, NameError):
    XMLIterable = None
from iterable.helpers.detect import detect_encoding_any, detect_file_type
from iterable.helpers.utils import detect_delimiter, detect_encoding_raw


class TestDetectors:
    def test_encoding(self):
        assert "utf-8" == detect_encoding_raw(filename="fixtures/ru_utf8_comma.csv")["encoding"]
        assert "windows-1251" == detect_encoding_raw(filename="fixtures/ru_cp1251_comma.csv")["encoding"]

    def test_encoding_any_raw(self):
        assert "utf-8" == detect_encoding_any(filename="fixtures/ru_utf8_comma.csv")["encoding"]
        assert "windows-1251" == detect_encoding_any(filename="fixtures/ru_cp1251_comma.csv")["encoding"]

    def test_encoding_any_compressed(self):
        #        assert 'utf-8' == detect_encoding_any(filename='fixtures/ru_utf8_comma.csv.zst')['encoding']
        assert "windows-1251" == detect_encoding_any(filename="fixtures/ru_cp1251_comma.csv.gz")["encoding"]

    def test_delimiters(self):
        assert "," == detect_delimiter(filename="fixtures/ru_utf8_comma.csv")
        assert ";" == detect_delimiter(filename="fixtures/ru_utf8_semicolon.csv")
        assert "\t" == detect_delimiter(filename="fixtures/ru_utf8_tab.csv")

    def test_filetype_csv_utf8(self):
        result = detect_file_type("fixtures/9_25.24.28.712_2014.csv")
        assert result["success"]
        assert result["datatype"] == CSVIterable
        assert result["codec"] is None

    def test_filetype_plain_csv(self):
        result = detect_file_type("fixtures/ru_utf8_comma.csv")
        assert result["success"]
        assert result["datatype"] == CSVIterable
        assert result["codec"] is None

    def test_filetype_bson(self):
        result = detect_file_type("fixtures/2cols6rows_flat.bson")
        assert result["success"]
        assert result["datatype"] == BSONIterable
        assert result["codec"] is None

    def test_filetype_pickle(self):
        result = detect_file_type("fixtures/2cols6rows_flat.pickle")
        assert result["success"]
        assert result["datatype"] == PickleIterable
        assert result["codec"] is None

    def test_filetype_json(self):
        result = detect_file_type("fixtures/2cols6rows_array.json")
        assert result["success"]
        assert result["datatype"] == JSONIterable
        assert result["codec"] is None

    def test_filetype_jsonl(self):
        result = detect_file_type("fixtures/2cols6rows_flat.jsonl")
        assert result["success"]
        assert result["datatype"] == JSONLinesIterable
        assert result["codec"] is None

    def test_filetype_ndjson(self):
        result = detect_file_type("fixtures/2cols6rows_flat.ndjson")
        assert result["success"]
        assert result["datatype"] == JSONLinesIterable
        assert result["codec"] is None

    def test_filetype_avro(self):
        result = detect_file_type("fixtures/2cols6rows.avro")
        assert result["success"]
        assert result["datatype"] == AVROIterable
        assert result["codec"] is None

    def test_filetype_orc(self):
        result = detect_file_type("fixtures/2cols6rows.orc")
        assert result["success"]
        assert result["datatype"] == ORCIterable
        assert result["codec"] is None

    def test_filetype_xml(self):
        result = detect_file_type("fixtures/books.xml")
        assert result["success"]
        assert result["datatype"] == XMLIterable
        assert result["codec"] is None

    def test_filetype_zstd_csv(self):
        if ZSTDCodec is None:
            pytest.skip("ZSTDCodec not available")
        result = detect_file_type("fixtures/2cols6rows.csv.zst")
        assert result["success"]
        assert result["datatype"] == CSVIterable
        assert result["codec"] == ZSTDCodec

    def test_filetype_brotli_csv(self):
        if BrotliCodec is None:
            pytest.skip("BrotliCodec not available")
        result = detect_file_type("fixtures/2cols6rows.csv.br")
        assert result["success"]
        assert result["datatype"] == CSVIterable
        assert result["codec"] == BrotliCodec

    def test_filetype_bzip2_csv(self):
        result = detect_file_type("fixtures/2cols6rows.csv.bz2")
        assert result["success"]
        assert result["datatype"] == CSVIterable
        assert result["codec"] == BZIP2Codec

    def test_filetype_lzma_csv(self):
        result = detect_file_type("fixtures/2cols6rows.csv.xz")
        assert result["success"]
        assert result["datatype"] == CSVIterable
        assert result["codec"] == LZMACodec

    def test_filetype_gzip_csv(self):
        result = detect_file_type("fixtures/2cols6rows.csv.gz")
        assert result["success"]
        assert result["datatype"] == CSVIterable
        assert result["codec"] == GZIPCodec

    def test_filetype_plain_parquet(self):
        result = detect_file_type("fixtures/2cols6rows.parquet")
        assert result["success"]
        assert result["datatype"] == ParquetIterable
        assert result["codec"] is None

    def test_filetype_plain_xls(self):
        result = detect_file_type("fixtures/2cols6rows.xls")
        assert result["success"]
        assert result["datatype"] == XLSIterable
        assert result["codec"] is None

    def test_filetype_plain_xlsx(self):
        result = detect_file_type("fixtures/2cols6rows.xlsx")
        assert result["success"]
        assert result["datatype"] == XLSXIterable
        assert result["codec"] is None

    def test_filetype_plain_dbf(self):
        result = detect_file_type("fixtures/2cols6rows.dbf")
        assert result["success"]
        assert result["datatype"] == DBFIterable
        assert result["codec"] is None

    def test_filetype_duckdb(self):
        """Test DuckDB format detection"""
        import tempfile

        import duckdb

        with tempfile.NamedTemporaryFile(delete=False, suffix=".duckdb") as tmp:
            tmp_path = tmp.name
        # DuckDB may error if the file already exists but is empty/not a valid DB.
        # Ensure the path does not exist before connecting so DuckDB can create it.
        import os

        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

        try:
            # Create a minimal DuckDB database file
            conn = duckdb.connect(tmp_path)
            conn.execute("CREATE TABLE test (id INTEGER)")
            if hasattr(conn, "commit"):
                conn.commit()
            conn.close()

            result = detect_file_type(tmp_path)
            assert result["success"]
            assert result["datatype"] == DuckDBIterable
            assert result["codec"] is None
        finally:
            import os

            if os.path.exists(tmp_path):
                os.unlink(tmp_path)

    def test_filetype_ddb(self):
        """Test DuckDB format detection with .ddb extension"""
        import tempfile

        import duckdb

        with tempfile.NamedTemporaryFile(delete=False, suffix=".ddb") as tmp:
            tmp_path = tmp.name
        # DuckDB may error if the file already exists but is empty/not a valid DB.
        # Ensure the path does not exist before connecting so DuckDB can create it.
        import os

        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

        try:
            # Create a minimal DuckDB database file
            conn = duckdb.connect(tmp_path)
            conn.execute("CREATE TABLE test (id INTEGER)")
            if hasattr(conn, "commit"):
                conn.commit()
            conn.close()

            result = detect_file_type(tmp_path)
            assert result["success"]
            assert result["datatype"] == DuckDBIterable
            assert result["codec"] is None
        finally:
            import os

            if os.path.exists(tmp_path):
                os.unlink(tmp_path)

    def test_detect_file_type_empty_filename(self):
        """Test detect_file_type with empty filename raises ValueError"""
        with pytest.raises(ValueError, match="Filename cannot be empty"):
            detect_file_type("")

    def test_detect_file_type_unknown_format(self):
        """Test detect_file_type with unknown format"""
        result = detect_file_type("test.unknown")
        assert result["success"] is False
        assert result["datatype"] is None
        assert result["codec"] is None

    def test_detect_file_type_no_extension(self):
        """Test detect_file_type with no extension"""
        result = detect_file_type("test")
        assert result["success"] is False

    def test_detect_compression_empty_filename(self):
        """Test detect_compression with empty filename raises ValueError"""
        from iterable.helpers.detect import detect_compression

        with pytest.raises(ValueError, match="Filename cannot be empty"):
            detect_compression("")

    def test_detect_compression_gzip(self):
        """Test detect_compression with gzip file"""
        from iterable.helpers.detect import detect_compression

        result = detect_compression("test.csv.gz")
        assert result["success"] is True
        assert result["compression"] == GZIPCodec
        assert result["codec"] is None
        assert result["datatype"] is None

    def test_detect_compression_bz2(self):
        """Test detect_compression with bz2 file"""
        from iterable.helpers.detect import detect_compression

        result = detect_compression("test.bz2")
        assert result["success"] is True
        assert result["compression"] == BZIP2Codec

    def test_detect_compression_no_compression(self):
        """Test detect_compression with no compression"""
        from iterable.helpers.detect import detect_compression

        result = detect_compression("test.csv")
        assert result["success"] is False
        assert result["compression"] is None

    def test_detect_encoding_any_empty_filename(self):
        """Test detect_encoding_any with empty filename raises ValueError"""
        with pytest.raises(ValueError, match="Filename cannot be empty"):
            detect_encoding_any("")

    def test_detect_encoding_any_file_not_found(self):
        """Test detect_encoding_any with non-existent file raises FileNotFoundError"""
        import os

        non_existent = "this_file_does_not_exist_12345.txt"
        assert not os.path.exists(non_existent)
        with pytest.raises(FileNotFoundError):
            detect_encoding_any(non_existent)

    def test_detect_encoding_any_empty_file(self, tmp_path):
        """Test detect_encoding_any with empty file raises ValueError"""
        empty_file = tmp_path / "empty.txt"
        empty_file.write_text("")
        with pytest.raises(ValueError, match="appears to be empty"):
            detect_encoding_any(str(empty_file))

    def test_open_iterable_empty_filename(self):
        """Test open_iterable with empty filename raises ValueError"""
        from iterable.helpers.detect import open_iterable

        with pytest.raises(ValueError, match="Filename cannot be empty"):
            open_iterable("")

    def test_open_iterable_invalid_engine(self):
        """Test open_iterable with invalid engine raises ValueError"""
        from iterable.helpers.detect import open_iterable

        with pytest.raises(ValueError, match="Engine must be 'internal' or 'duckdb'"):
            open_iterable("test.csv", engine="invalid")

    def test_open_iterable_file_not_found(self):
        """Test open_iterable with non-existent file raises FileNotFoundError"""
        import os

        from iterable.helpers.detect import open_iterable

        non_existent = "this_file_does_not_exist_12345.csv"
        assert not os.path.exists(non_existent)
        with pytest.raises(FileNotFoundError):
            open_iterable(non_existent)

    def test_open_iterable_unknown_format(self, tmp_path):
        """Test open_iterable with unknown format raises RuntimeError"""
        from iterable.helpers.detect import open_iterable

        unknown_file = tmp_path / "test.unknown"
        unknown_file.write_text("test data")

        with pytest.raises(RuntimeError, match="Could not detect file type"):
            open_iterable(str(unknown_file))

    def test_open_iterable_duckdb_unsupported_format(self, tmp_path):
        """Test open_iterable with DuckDB engine and unsupported format"""
        from iterable.helpers.detect import open_iterable

        xml_file = tmp_path / "test.xml"
        xml_file.write_text("<root><item>test</item></root>")

        with pytest.raises(ValueError, match="DuckDB engine does not support file type"):
            open_iterable(str(xml_file), engine="duckdb")

    def test_is_flat_function(self):
        """Test is_flat function"""
        from iterable.helpers.detect import is_flat

        # Test with filename
        assert is_flat(filename="test.csv") is True
        assert is_flat(filename="test.json") is False
        # Test with filetype
        assert is_flat(filetype="csv") is True
        assert is_flat(filetype="json") is False
        # Test with None
        assert is_flat() is False

    def test_is_flat_with_compressed(self):
        """Test is_flat with compressed file"""
        from iterable.helpers.detect import is_flat

        assert is_flat(filename="test.csv.gz") is True
        assert is_flat(filename="test.json.gz") is False

    def test_open_iterable_basic(self):
        """Test open_iterable with basic file"""
        from iterable.helpers.detect import open_iterable

        iterable = open_iterable("fixtures/2cols6rows.csv")
        assert iterable is not None
        iterable.close()

    def test_open_iterable_with_mode(self):
        """Test open_iterable with write mode"""
        import os

        from iterable.helpers.detect import open_iterable

        os.makedirs("testdata", exist_ok=True)
        iterable = open_iterable("testdata/test_open_write.csv", mode="w")
        assert iterable is not None
        iterable.close()
        if os.path.exists("testdata/test_open_write.csv"):
            os.unlink("testdata/test_open_write.csv")

    def test_open_iterable_with_iterableargs(self):
        """Test open_iterable with iterableargs"""
        from iterable.helpers.detect import open_iterable

        iterable = open_iterable("fixtures/ru_utf8_semicolon.csv", iterableargs={"delimiter": ";"})
        assert iterable is not None
        row = iterable.read()
        assert row is not None
        iterable.close()

    def test_open_iterable_unknown_format(self):
        """Test open_iterable with unknown format"""
        from iterable.helpers.detect import open_iterable

        with pytest.raises((ValueError, ImportError)):
            open_iterable("test.unknown_format_xyz")

    def test_detect_file_type_from_content(self, tmp_path):
        """Test detect_file_type_from_content with various formats"""
        from iterable.helpers.detect import detect_file_type_from_content

        # Test JSON detection
        json_file = tmp_path / "test.json"
        json_file.write_text('{"name": "test"}')
        with open(json_file, "rb") as f:
            result = detect_file_type_from_content(f)
        assert result == "json"

        # Test CSV detection
        csv_file = tmp_path / "test.csv"
        csv_file.write_text("id,name\n1,test")
        with open(csv_file, "rb") as f:
            result = detect_file_type_from_content(f)
        assert result in ["csv", "tsv"]  # May detect as CSV or TSV

    def test_detect_file_type_empty_filename(self):
        """Test detect_file_type with empty filename"""
        from iterable.helpers.detect import detect_file_type

        with pytest.raises(ValueError, match="Filename cannot be empty"):
            detect_file_type("")

    def test_detect_file_type_with_fileobj(self, tmp_path):
        """Test detect_file_type with fileobj"""
        from iterable.helpers.detect import detect_file_type

        csv_file = tmp_path / "test.csv"
        csv_file.write_text("id,name\n1,test")
        with open(csv_file, "rb") as f:
            result = detect_file_type(str(csv_file), fileobj=f)
        assert result["success"]
        assert result["datatype"] == CSVIterable

    def test_load_symbol_error_handling(self):
        """Test _load_symbol error handling"""
        from iterable.helpers.detect import _load_symbol

        # Test with invalid module
        with pytest.raises(ImportError):
            _load_symbol("nonexistent.module", "NonExistentClass")

    def test_datatype_class(self):
        """Test _datatype_class function"""
        from iterable.helpers.detect import _datatype_class

        csv_class = _datatype_class("csv")
        assert csv_class == CSVIterable

    def test_codec_class(self):
        """Test _codec_class function"""
        from iterable.helpers.detect import _codec_class

        gzip_class = _codec_class("gz")
        assert gzip_class == GZIPCodec

    def test_detect_compression_unknown(self):
        """Test detect_compression with unknown format"""
        from iterable.helpers.detect import detect_compression

        result = detect_compression("test.unknown")
        assert result["success"] is False
        assert result["compression"] is None

    def test_detect_encoding_any_with_limit(self, tmp_path):
        """Test detect_encoding_any with custom limit"""
        from iterable.helpers.detect import detect_encoding_any

        test_file = tmp_path / "test.txt"
        test_file.write_text("test content" * 1000)
        result = detect_encoding_any(str(test_file), limit=100)
        assert "encoding" in result
