"""Tests for goldenmatch smart ingest preprocessor."""

from __future__ import annotations

from pathlib import Path

import polars as pl
import pytest

from goldenmatch.core.smart_ingest import (
    detect_blocks,
    detect_delimiter,
    detect_encoding,
    detect_fixed_width,
    detect_header_row,
    detect_key_value,
    detect_skip_rows,
    extract_entities,
    parse_blocks,
    parse_fixed_width,
    parse_key_value,
    smart_load,
)
from goldenmatch.core.ingest import load_file


# ---------------------------------------------------------------------------
# Encoding Detection
# ---------------------------------------------------------------------------


class TestDetectEncoding:
    def test_utf8(self, tmp_path):
        p = tmp_path / "utf8.txt"
        p.write_text("hello world\n", encoding="utf-8")
        assert detect_encoding(p) == "utf8"

    def test_utf8_bom(self, tmp_path):
        p = tmp_path / "bom.txt"
        p.write_bytes(b"\xef\xbb\xbfhello world\n")
        assert detect_encoding(p) == "utf8"

    def test_latin1(self, tmp_path):
        p = tmp_path / "latin1.txt"
        # Write bytes that are valid latin-1 but invalid utf-8
        p.write_bytes(b"caf\xe9\n")
        enc = detect_encoding(p)
        # Should still return utf8 (our fallback) since latin-1 without
        # mojibake patterns is ambiguous
        assert enc == "utf8"

    def test_utf16_le_bom(self, tmp_path):
        p = tmp_path / "utf16.txt"
        p.write_bytes(b"\xff\xfeh\x00i\x00")
        assert detect_encoding(p) == "utf-16"


# ---------------------------------------------------------------------------
# Delimiter Sniffing
# ---------------------------------------------------------------------------


class TestDetectDelimiter:
    def test_comma(self):
        lines = ["a,b,c", "1,2,3", "4,5,6"]
        assert detect_delimiter(lines) == ","

    def test_tab(self):
        lines = ["a\tb\tc", "1\t2\t3", "4\t5\t6"]
        assert detect_delimiter(lines) == "\t"

    def test_pipe(self):
        lines = ["a|b|c", "1|2|3", "4|5|6"]
        assert detect_delimiter(lines) == "|"

    def test_semicolon(self):
        lines = ["a;b;c", "1;2;3", "4;5;6"]
        assert detect_delimiter(lines) == ";"

    def test_mixed_picks_most_consistent(self):
        # Tabs appear consistently, commas appear in data values
        lines = [
            "name\tage\tcity",
            "John, Jr.\t30\tNew York, NY",
            "Jane\t25\tBoston, MA",
        ]
        assert detect_delimiter(lines) == "\t"

    def test_empty_lines_default(self):
        lines = ["", "", ""]
        assert detect_delimiter(lines) == ","

    def test_no_delimiters_default(self):
        lines = ["hello", "world", "test"]
        assert detect_delimiter(lines) == ","


# ---------------------------------------------------------------------------
# Header Detection
# ---------------------------------------------------------------------------


class TestDetectHeaderRow:
    def test_header_on_row_0(self):
        lines = ["name,age,city", "John,30,NYC", "Jane,25,Boston"]
        idx, has = detect_header_row(lines, ",")
        assert idx == 0
        assert has is True

    def test_header_on_row_3(self):
        lines = [
            "Report generated 2024-01-01",
            "---",
            "",
            "name,age,city",
            "John,30,NYC",
        ]
        idx, has = detect_header_row(lines, ",")
        assert idx == 3
        assert has is True

    def test_no_header(self):
        lines = ["1,2,3", "4,5,6", "7,8,9"]
        idx, has = detect_header_row(lines, ",")
        assert has is False


# ---------------------------------------------------------------------------
# Skip Rows
# ---------------------------------------------------------------------------


class TestDetectSkipRows:
    def test_junk_before_header(self):
        lines = [
            "Generated by System v2.1",
            "---",
            "name,age",
            "John,30",
        ]
        skips = detect_skip_rows(lines, ",", header_row=2)
        assert 0 in skips
        assert 1 in skips
        assert 2 not in skips
        assert 3 not in skips

    def test_separator_lines(self):
        lines = [
            "name,age",
            "--------",
            "John,30",
        ]
        skips = detect_skip_rows(lines, ",", header_row=0)
        assert 1 in skips

    def test_end_of_file(self):
        lines = [
            "name,age",
            "John,30",
            "END OF FILE",
        ]
        skips = detect_skip_rows(lines, ",", header_row=0)
        assert 2 in skips

    def test_empty_lines(self):
        lines = ["name,age", "", "John,30", ""]
        skips = detect_skip_rows(lines, ",", header_row=0)
        assert 1 in skips
        assert 3 in skips


# ---------------------------------------------------------------------------
# Fixed-Width
# ---------------------------------------------------------------------------


class TestFixedWidth:
    def test_detect_fixed_width_columns(self):
        lines = [
            "Name       Age  City      ",
            "John        30  New York  ",
            "Jane        25  Boston    ",
            "Bob         40  Chicago   ",
        ]
        boundaries = detect_fixed_width(lines)
        assert boundaries is not None
        assert len(boundaries) >= 3

    def test_parse_fixed_width_with_header(self):
        lines = [
            "Name       Age  City      ",
            "John        30  New York  ",
            "Jane        25  Boston    ",
        ]
        boundaries = detect_fixed_width(lines)
        assert boundaries is not None
        df = parse_fixed_width(lines, boundaries, has_header=True)
        assert len(df) == 2
        assert df.columns[0] == "Name"

    def test_parse_fixed_width_without_header(self):
        lines = [
            "John        30  New York  ",
            "Jane        25  Boston    ",
            "Bob         40  Chicago   ",
            "Alice       35  Denver    ",
        ]
        boundaries = detect_fixed_width(lines)
        assert boundaries is not None
        df = parse_fixed_width(lines, boundaries, has_header=False)
        assert len(df) == 4
        assert df.columns[0] == "column_1"

    def test_detect_returns_none_for_delimited(self):
        lines = ["a,b,c", "1,2,3", "4,5,6"]
        result = detect_fixed_width(lines)
        # Could be None or not — just verify no crash
        assert result is None or isinstance(result, list)


# ---------------------------------------------------------------------------
# Key-Value
# ---------------------------------------------------------------------------


class TestKeyValue:
    def test_detect_colon(self):
        lines = [
            "Name: John Smith",
            "Age: 30",
            "City: New York",
        ]
        result = detect_key_value(lines)
        assert result is not None
        sep, keys = result
        assert sep == ":"
        assert "Name" in keys
        assert "Age" in keys

    def test_detect_equals(self):
        lines = [
            "name=John",
            "age=30",
            "city=NY",
        ]
        result = detect_key_value(lines)
        assert result is not None
        sep, keys = result
        assert sep == "="

    def test_parse_multiple_records(self):
        text = (
            "Name: John Smith\n"
            "Age: 30\n"
            "City: New York\n"
            "Name: Jane Doe\n"
            "Age: 25\n"
            "City: Boston\n"
        )
        df = parse_key_value(text, ":", ["Name", "Age", "City"])
        assert len(df) == 2
        assert "Name" in df.columns
        assert df["Name"][0] == "John Smith"
        assert df["Name"][1] == "Jane Doe"

    def test_detect_returns_none_for_csv(self):
        lines = ["a,b,c", "1,2,3"]
        assert detect_key_value(lines) is None


# ---------------------------------------------------------------------------
# Blocks
# ---------------------------------------------------------------------------


class TestBlocks:
    def test_detect_blocks(self):
        lines = [
            "John Smith",
            "30",
            "New York",
            "",
            "Jane Doe",
            "25",
            "Boston",
        ]
        blocks = detect_blocks(lines)
        assert blocks is not None
        assert len(blocks) == 2

    def test_parse_positional_blocks(self):
        lines = [
            "John Smith",
            "30",
            "New York",
            "",
            "Jane Doe",
            "25",
            "Boston",
        ]
        blocks = detect_blocks(lines)
        assert blocks is not None
        df = parse_blocks(blocks)
        assert len(df) == 2
        assert df.columns == ["column_1", "column_2", "column_3"]

    def test_parse_kv_blocks(self):
        lines = [
            "Name: John",
            "Age: 30",
            "",
            "Name: Jane",
            "Age: 25",
        ]
        blocks = detect_blocks(lines)
        assert blocks is not None
        df = parse_blocks(blocks)
        assert len(df) == 2
        assert "Name" in df.columns

    def test_detect_returns_none_single_block(self):
        lines = ["line1", "line2", "line3"]
        assert detect_blocks(lines) is None


# ---------------------------------------------------------------------------
# Entity Extraction
# ---------------------------------------------------------------------------


class TestEntityExtraction:
    def test_email(self):
        text = "Contact john@example.com for info.\nOr jane@test.com."
        df = extract_entities(text)
        assert "email" in df.columns
        emails = df["email"].to_list()
        assert "john@example.com" in emails

    def test_phone(self):
        text = "Call 267-555-1234 or (212) 555-9999."
        df = extract_entities(text)
        assert "phone" in df.columns

    def test_zip(self):
        text = "Shipping to 19382 or 90210-1234."
        df = extract_entities(text)
        assert "zip" in df.columns
        zips = df["zip"].to_list()
        assert any("19382" in z for z in zips)

    def test_name_extraction(self):
        text = "Contact John Smith at john@example.com."
        df = extract_entities(text)
        assert "name" in df.columns or "email" in df.columns

    def test_empty_text(self):
        df = extract_entities("")
        assert len(df) == 0


# ---------------------------------------------------------------------------
# Smart Load
# ---------------------------------------------------------------------------


class TestSmartLoad:
    def test_auto_csv(self, tmp_path):
        p = tmp_path / "data.csv"
        p.write_text("name,age,city\nJohn,30,NYC\nJane,25,Boston\n")
        df, meta = smart_load(p)
        assert df.width >= 3
        assert len(df) == 2
        assert meta["encoding"] == "utf8"
        assert "detection_log" in meta

    def test_auto_tsv(self, tmp_path):
        p = tmp_path / "data.tsv"
        p.write_text("name\tage\tcity\nJohn\t30\tNYC\nJane\t25\tBoston\n")
        df, meta = smart_load(p)
        assert df.width >= 3
        assert len(df) == 2

    def test_auto_pipe(self, tmp_path):
        p = tmp_path / "data.txt"
        p.write_text("name|age|city\nJohn|30|NYC\nJane|25|Boston\n")
        df, meta = smart_load(p)
        assert df.width >= 3

    def test_explicit_delimited(self, tmp_path):
        p = tmp_path / "data.txt"
        p.write_text("name;age;city\nJohn;30;NYC\n")
        df, meta = smart_load(p, parse_mode="delimited")
        assert meta["parse_mode"] == "delimited"
        assert df.width >= 3

    def test_explicit_key_value(self, tmp_path):
        p = tmp_path / "data.txt"
        p.write_text("Name: John\nAge: 30\nCity: NYC\nName: Jane\nAge: 25\nCity: Boston\n")
        df, meta = smart_load(p, parse_mode="key_value")
        assert meta["parse_mode"] == "key_value"
        assert len(df) == 2

    def test_explicit_entity_extract(self, tmp_path):
        p = tmp_path / "notes.txt"
        p.write_text("Email john@example.com phone 267-555-1234\n")
        df, meta = smart_load(p, parse_mode="entity_extract")
        assert meta["parse_mode"] == "entity_extract"

    def test_metadata_dict(self, tmp_path):
        p = tmp_path / "data.csv"
        p.write_text("a,b\n1,2\n")
        _, meta = smart_load(p)
        assert "encoding" in meta
        assert "delimiter" in meta
        assert "header_row" in meta
        assert "has_header" in meta
        assert "skip_rows" in meta
        assert "parse_mode" in meta
        assert "detection_log" in meta

    def test_auto_kv_file(self, tmp_path):
        p = tmp_path / "data.txt"
        content = "Name: John\nAge: 30\nCity: NYC\nName: Jane\nAge: 25\nCity: Boston\n"
        p.write_text(content)
        df, meta = smart_load(p)
        assert len(df) >= 2

    def test_auto_block_file(self, tmp_path):
        p = tmp_path / "data.txt"
        content = "John Smith\n30\nNew York\n\nJane Doe\n25\nBoston\n"
        p.write_text(content)
        df, meta = smart_load(p)
        assert len(df) >= 2

    def test_messy_file_with_junk_header(self, tmp_path):
        p = tmp_path / "messy.csv"
        content = (
            "Report generated 2024-01-01\n"
            "---\n"
            "name,age,city\n"
            "John,30,NYC\n"
            "Jane,25,Boston\n"
            "END OF FILE\n"
        )
        p.write_text(content)
        df, meta = smart_load(p)
        # Should detect header and skip junk
        assert df.width >= 2
        assert len(df) >= 2


# ---------------------------------------------------------------------------
# load_file Integration
# ---------------------------------------------------------------------------


class TestLoadFileIntegration:
    def test_auto_detect_csv(self, tmp_path):
        p = tmp_path / "data.csv"
        p.write_text("name,age\nJohn,30\nJane,25\n")
        lf = load_file(p)
        assert isinstance(lf, pl.LazyFrame)
        df = lf.collect()
        assert "name" in df.columns
        assert len(df) == 2

    def test_explicit_delimiter_still_works(self, tmp_path):
        p = tmp_path / "data.csv"
        df = pl.DataFrame({"a": [1, 2], "b": ["x", "y"]})
        df.write_csv(p, separator="\t")
        lf = load_file(p, delimiter="\t")
        result = lf.collect()
        assert result.columns == ["a", "b"]

    def test_txt_file_auto(self, tmp_path):
        p = tmp_path / "data.txt"
        p.write_text("name|age|city\nJohn|30|NYC\nJane|25|Boston\n")
        lf = load_file(p, parse_mode="auto")
        assert isinstance(lf, pl.LazyFrame)
        df = lf.collect()
        assert df.width >= 3

    def test_tsv_auto(self, tmp_path):
        p = tmp_path / "data.tsv"
        p.write_text("name\tage\nJohn\t30\nJane\t25\n")
        lf = load_file(p)
        df = lf.collect()
        assert "name" in df.columns

    def test_parquet_unchanged(self, sample_parquet):
        lf = load_file(sample_parquet)
        assert isinstance(lf, pl.LazyFrame)
        df = lf.collect()
        assert len(df) == 3

    def test_excel_unchanged(self, tmp_path):
        p = tmp_path / "test.xlsx"
        df = pl.DataFrame({"id": [1, 2], "name": ["A", "B"]})
        df.write_excel(p)
        lf = load_file(p)
        assert isinstance(lf, pl.LazyFrame)

    def test_backwards_compat_positional_delimiter(self, tmp_path):
        """Existing code passing delimiter explicitly should still work."""
        p = tmp_path / "data.csv"
        df = pl.DataFrame({"x": [1], "y": [2]})
        df.write_csv(p, separator=",")
        lf = load_file(p, delimiter=",")
        result = lf.collect()
        assert result.columns == ["x", "y"]
