import gzip
import zipfile
from collections.abc import Generator
from pathlib import Path

import pytest
from pydantic import BaseModel, ValidationError

from pyimporters_plugins.base import (
    KnowledgeParserBase,
    KnowledgeParserModel,
    KnowledgeParserOptions,
    Term,
    is_gzipfile,
    maybe_archive,
)

TESTDATA = Path(__file__).parent / "data"


# --- concrete subclass for testing KnowledgeParserBase ---


class DummyParser(KnowledgeParserBase):
    def parse(self, source, options, bar):  # noqa: ARG002
        yield Term(identifier="id1")


# --- Term model ---


def test_term_minimal():
    term = Term(identifier="http://example.com/1")
    assert term.identifier == "http://example.com/1"
    assert term.lexicon is None
    assert term.preferredForm is None
    assert term.properties is None


def test_term_full():
    term = Term(
        identifier="http://example.com/1",
        lexicon="MeSH",
        preferredForm="rocks",
        properties={"altForms": ["basalt"], "wikidataId": "Q8063"},
    )
    assert term.lexicon == "MeSH"
    assert term.preferredForm == "rocks"
    assert term.properties["wikidataId"] == "Q8063"


# --- KnowledgeParserOptions ---


def test_parser():
    with pytest.raises(TypeError) as err:
        parser = KnowledgeParserBase()
        assert parser is None
    assert "KnowledgeParserBase" in str(err.value)
    assert "parse" in str(err.value)


def test_default_options():
    options = KnowledgeParserModel()
    assert options.project is None
    assert options.lexicon is None
    assert options.limit == 0
    assert options.lang == "en"


def test_options_with_values():
    options = KnowledgeParserOptions(project="myproject", lexicon="MeSH", lang="fr", limit=10)
    assert options.project == "myproject"
    assert options.lexicon == "MeSH"
    assert options.lang == "fr"
    assert options.limit == 10


def test_options_validation():
    with pytest.raises(ValidationError):
        options = KnowledgeParserModel(limit=-1)
        assert options is None


# --- KnowledgeParserBase class methods ---


def test_get_schema():
    assert DummyParser.get_schema() is KnowledgeParserOptions


def test_get_model():
    assert issubclass(DummyParser.get_model(), BaseModel)


def test_get_extensions():
    assert DummyParser.get_extensions() == []


def test_parse_yields_terms():
    parser = DummyParser()
    result = parser.parse(None, None, None)
    assert isinstance(result, Generator)
    terms = list(result)
    assert len(terms) == 1
    assert terms[0].identifier == "id1"


# --- is_gzipfile ---


def test_guess_zip():
    source = TESTDATA / "small.zip"
    assert zipfile.is_zipfile(source) is True
    source = TESTDATA / "small_zip"
    assert zipfile.is_zipfile(source) is True
    source = TESTDATA / "small.xml.gz"
    assert zipfile.is_zipfile(source) is False


def test_is_gzipfile_true():
    assert is_gzipfile(TESTDATA / "small.xml.gz") is True


def test_is_gzipfile_false():
    assert is_gzipfile(TESTDATA / "small.zip") is False


def test_is_gzipfile_nonexistent():
    with pytest.raises(AssertionError):
        is_gzipfile(Path("/nonexistent/file.gz"))


# --- maybe_archive ---


def test_maybe_archive_plain(tmp_path):
    f = tmp_path / "plain.txt"
    f.write_text("hello world")
    with maybe_archive(f) as fh:
        assert fh.read() == "hello world"


def test_maybe_archive_plain_binary(tmp_path):
    f = tmp_path / "plain.bin"
    f.write_bytes(b"hello")
    with maybe_archive(f, mode="rb") as fh:
        assert fh.read() == b"hello"


def test_maybe_archive_gz_extension():
    with maybe_archive(TESTDATA / "small.xml.gz") as fh:
        content = fh.read()
    assert len(content) > 0


def test_maybe_archive_gz_magic(tmp_path):
    # gzip file without .gz extension — detected by magic bytes
    gz_path = tmp_path / "no_extension"
    with gzip.open(gz_path, "wb") as f:
        f.write(b"gzip content")
    with maybe_archive(gz_path, mode="rb") as fh:
        assert fh.read() == b"gzip content"


def test_maybe_archive_zip_single_text():
    with maybe_archive(TESTDATA / "small.zip") as fh:
        content = fh.read()
    assert len(content) > 0


def test_maybe_archive_zip_single_binary():
    with maybe_archive(TESTDATA / "small.zip", mode="rb") as fh:
        content = fh.read()
    assert len(content) > 0


def test_maybe_archive_zip_no_extension():
    with maybe_archive(TESTDATA / "small_zip") as fh:
        content = fh.read()
    assert len(content) > 0


def test_maybe_archive_zip_multi(tmp_path):
    # zip with multiple files — yields the ZipFile itself
    zpath = tmp_path / "multi.zip"
    with zipfile.ZipFile(zpath, "w") as zf:
        zf.writestr("a.txt", "file a")
        zf.writestr("b.txt", "file b")
    with maybe_archive(zpath) as fh:
        assert isinstance(fh, zipfile.ZipFile)
        names = fh.namelist()
    assert sorted(names) == ["a.txt", "b.txt"]


def test_maybe_archive_xlsx_like(tmp_path):
    # zip containing [Content_Types].xml (xlsx) — falls back to plain open
    xpath = tmp_path / "fake.xlsx"
    with zipfile.ZipFile(xpath, "w") as zf:
        zf.writestr("[Content_Types].xml", "<Types/>")
        zf.writestr("sheet.xml", "<sheet/>")
    with maybe_archive(xpath, mode="rb") as fh:
        data = fh.read()
    assert len(data) > 0
