"""
Comprehensive test suite for LineSource.

Tests cover:
- Initialization with different source types
- File path handling (string and Path)
- File-like object handling (open files, StringIO)
- Iterable handling (lists, generators, tuples)
- Newline normalization across different line endings
- Edge cases (empty sources, single lines, mixed newlines)
- Error handling for unsupported source types
- Unicode and encoding handling
- Integration with real files
"""

import pytest
from pathlib import Path
from io import StringIO
from atomingest.normalization.io import LineSource


class TestLineSourceInitialization:
    """Test initialization of LineSource with different source types."""

    def test_init_with_string_path(self):
        """Test initialization with a string file path."""
        source = LineSource("test.txt")
        assert source.source == "test.txt"

    def test_init_with_path_object(self):
        """Test initialization with a Path object."""
        path = Path("test.txt")
        source = LineSource(path)
        assert source.source == path

    def test_init_with_file_like_object(self):
        """Test initialization with a file-like object."""
        file_obj = StringIO("line1\nline2\n")
        source = LineSource(file_obj)
        assert source.source is file_obj

    def test_init_with_list(self):
        """Test initialization with a list of strings."""
        lines = ["line1", "line2", "line3"]
        source = LineSource(lines)
        assert source.source == lines

    def test_init_with_tuple(self):
        """Test initialization with a tuple of strings."""
        lines = ("line1", "line2", "line3")
        source = LineSource(lines)
        assert source.source == lines

    def test_init_with_generator(self):
        """Test initialization with a generator."""

        def gen():
            yield "line1"
            yield "line2"

        generator = gen()
        source = LineSource(generator)
        assert source.source is generator


class TestLineSourceFromStringPath:
    """Test LineSource when initialized with string file paths."""

    def test_iterate_from_string_path(self, temp_dir):
        """Test iterating over lines from a file specified by string path."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("line1\nline2\nline3\n", encoding="utf-8")

        source = LineSource(str(file_path))
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_string_path_no_trailing_newline(self, temp_dir):
        """Test file without trailing newline."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("line1\nline2\nline3", encoding="utf-8")

        source = LineSource(str(file_path))
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_string_path_single_line(self, temp_dir):
        """Test file with single line."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("single line", encoding="utf-8")

        source = LineSource(str(file_path))
        lines = list(source)

        assert lines == ["single line"]

    def test_iterate_from_string_path_empty_file(self, temp_dir):
        """Test empty file."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("", encoding="utf-8")

        source = LineSource(str(file_path))
        lines = list(source)

        assert lines == []

    def test_file_not_found_raises_error(self):
        """Test that non-existent file raises FileNotFoundError."""
        source = LineSource("nonexistent_file.txt")

        with pytest.raises(FileNotFoundError):
            list(source)


class TestLineSourceFromPathObject:
    """Test LineSource when initialized with Path objects."""

    def test_iterate_from_path_object(self, temp_dir):
        """Test iterating over lines from a file specified by Path object."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("line1\nline2\nline3\n", encoding="utf-8")

        source = LineSource(file_path)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_path_object_relative_path(self, temp_dir, monkeypatch):
        """Test using relative Path object."""
        monkeypatch.chdir(temp_dir)
        file_path = Path("test.txt")
        file_path.write_text("content\n", encoding="utf-8")

        source = LineSource(file_path)
        lines = list(source)

        assert lines == ["content"]

    def test_path_object_absolute_path(self, temp_dir):
        """Test using absolute Path object."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("absolute path test\n", encoding="utf-8")

        source = LineSource(file_path.resolve())
        lines = list(source)

        assert lines == ["absolute path test"]


class TestLineSourceFromFileLikeObject:
    """Test LineSource when initialized with file-like objects."""

    def test_iterate_from_stringio(self):
        """Test iterating over lines from StringIO object."""
        file_obj = StringIO("line1\nline2\nline3\n")
        source = LineSource(file_obj)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_stringio_no_trailing_newline(self):
        """Test StringIO without trailing newline."""
        file_obj = StringIO("line1\nline2\nline3")
        source = LineSource(file_obj)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_stringio_empty(self):
        """Test empty StringIO."""
        file_obj = StringIO("")
        source = LineSource(file_obj)
        lines = list(source)

        assert lines == []

    def test_iterate_from_stringio_single_line(self):
        """Test StringIO with single line."""
        file_obj = StringIO("single line")
        source = LineSource(file_obj)
        lines = list(source)

        assert lines == ["single line"]

    def test_iterate_from_open_file(self, temp_dir):
        """Test iterating from an open file object."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("line1\nline2\nline3\n", encoding="utf-8")

        with open(file_path, "r", encoding="utf-8") as f:
            source = LineSource(f)
            lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_stringio_can_be_iterated_multiple_times(self):
        """Test that a fresh StringIO can be used to create multiple LineSource instances."""
        content = "line1\nline2\n"

        file_obj1 = StringIO(content)
        source1 = LineSource(file_obj1)
        lines1 = list(source1)

        file_obj2 = StringIO(content)
        source2 = LineSource(file_obj2)
        lines2 = list(source2)

        assert lines1 == lines2 == ["line1", "line2"]


class TestLineSourceFromIterable:
    """Test LineSource when initialized with iterables."""

    def test_iterate_from_list(self):
        """Test iterating over lines from a list."""
        lines_input = ["line1", "line2", "line3"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_tuple(self):
        """Test iterating over lines from a tuple."""
        lines_input = ("line1", "line2", "line3")
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_generator(self):
        """Test iterating over lines from a generator."""

        def line_generator():
            yield "line1"
            yield "line2"
            yield "line3"

        source = LineSource(line_generator())
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_iterate_from_empty_list(self):
        """Test iterating over empty list."""
        source = LineSource([])
        lines = list(source)

        assert lines == []

    def test_iterate_from_single_item_list(self):
        """Test iterating over single-item list."""
        source = LineSource(["single line"])
        lines = list(source)

        assert lines == ["single line"]

    def test_iterate_from_set(self):
        """Test iterating over a set (order may vary)."""
        lines_input = {"line1", "line2", "line3"}
        source = LineSource(lines_input)
        lines = list(source)

        assert len(lines) == 3
        assert set(lines) == {"line1", "line2", "line3"}

    def test_generator_exhausted_after_iteration(self):
        """Test that generator is exhausted after iteration."""

        def line_generator():
            yield "line1"
            yield "line2"

        gen = line_generator()
        source = LineSource(gen)

        list(source)  # First iteration
        lines_second = list(source)  # Second iteration

        # Generator is exhausted, should return empty
        assert lines_second == []


class TestLineSourceNewlineNormalization:
    """Test newline normalization across different line ending styles."""

    def test_normalize_unix_newlines(self):
        """Test normalization of Unix-style newlines (\\n)."""
        lines_input = ["line1\n", "line2\n", "line3\n"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_normalize_windows_newlines(self):
        """Test normalization of Windows-style newlines (\\r\\n)."""
        lines_input = ["line1\r\n", "line2\r\n", "line3\r\n"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_normalize_old_mac_newlines(self):
        """Test normalization of old Mac-style newlines (\\r)."""
        lines_input = ["line1\r", "line2\r", "line3\r"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_normalize_mixed_newlines(self):
        """Test normalization of mixed newline styles."""
        lines_input = ["line1\n", "line2\r\n", "line3\r", "line4"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3", "line4"]

    def test_normalize_multiple_trailing_newlines(self):
        """Test normalization of multiple trailing newlines."""
        lines_input = ["line1\n\n", "line2\r\n\r\n", "line3"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_lines_without_newlines_unchanged(self):
        """Test that lines without newlines are not modified."""
        lines_input = ["line1", "line2", "line3"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_normalize_stringio_with_windows_newlines(self):
        """Test normalization from StringIO with Windows newlines."""
        file_obj = StringIO("line1\r\nline2\r\nline3\r\n")
        source = LineSource(file_obj)
        lines = list(source)

        assert lines == ["line1", "line2", "line3"]

    def test_normalize_file_with_mixed_newlines(self, temp_dir):
        """Test normalization from file with mixed newlines."""
        file_path = temp_dir / "test.txt"
        # Write binary to preserve exact newlines
        file_path.write_bytes(b"line1\nline2\r\nline3\rline4")

        source = LineSource(str(file_path))
        lines = list(source)

        # Python's universal newlines handling converts all to \n, then we strip
        assert all(
            line in ["line1", "line2", "line3", "line4", "line3\rline4"]
            for line in lines
        )


class TestLineSourceEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_empty_string_lines(self):
        """Test handling of empty string lines."""
        lines_input = ["", "", ""]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["", "", ""]

    def test_whitespace_only_lines(self):
        """Test handling of whitespace-only lines."""
        lines_input = ["   ", "\t", "  \t  "]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["   ", "\t", "  \t  "]

    def test_very_long_lines(self):
        """Test handling of very long lines."""
        long_line = "x" * 10000
        lines_input = [long_line + "\n"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == [long_line]
        assert len(lines[0]) == 10000

    def test_unicode_characters(self):
        """Test handling of Unicode characters."""
        lines_input = ["héllo\n", "世界\n", "🎉\n"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["héllo", "世界", "🎉"]

    def test_unicode_from_file(self, temp_dir):
        """Test reading Unicode characters from file."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("héllo\n世界\n🎉\n", encoding="utf-8")

        source = LineSource(str(file_path))
        lines = list(source)

        assert lines == ["héllo", "世界", "🎉"]

    def test_special_characters_in_lines(self):
        """Test handling of special characters."""
        lines_input = ["line\twith\ttabs\n", "line with spaces\n", "line|with|pipes\n"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line\twith\ttabs", "line with spaces", "line|with|pipes"]

    def test_lines_with_null_bytes_rejected(self):
        """Test that null bytes in lines don't cause issues."""
        lines_input = ["line1\n", "line2\n"]  # Normal lines should work
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "line2"]

    def test_line_with_only_newline(self):
        """Test handling of lines that are only newlines."""
        lines_input = ["\n", "\r\n", "\r"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["", "", ""]


class TestLineSourceErrorHandling:
    """Test error handling for invalid inputs."""

    def test_unsupported_integer_source_raises_error(self):
        """Test that integer source raises ValueError."""
        source = LineSource(42)

        with pytest.raises(ValueError, match="Unsupported source type"):
            list(source)

    def test_dict_source_iterates_over_keys(self):
        """Test that dict source is treated as iterable (yields keys)."""
        source = LineSource({"key": "value", "another": "test"})
        lines = list(source)

        # Dicts are iterable and yield their keys
        assert set(lines) == {"key", "another"}

    def test_unsupported_none_source_raises_error(self):
        """Test that None source raises ValueError."""
        source = LineSource(None)

        with pytest.raises(ValueError, match="Unsupported source type"):
            list(source)

    def test_string_as_iterable_rejected(self):
        """Test that plain strings are treated as file paths, not iterables."""
        # A string that looks like a path but doesn't exist
        source = LineSource("not_a_real_file.txt")

        with pytest.raises(FileNotFoundError):
            list(source)

    def test_bytes_source_raises_error(self):
        """Test that bytes source raises ValueError."""
        source = LineSource(b"bytes content")

        with pytest.raises(ValueError, match="Unsupported source type"):
            list(source)


class TestLineSourceHelperMethods:
    """Test internal helper methods."""

    def test_is_file_path_with_string(self):
        """Test _is_file_path identifies string paths correctly."""
        source = LineSource("test.txt")
        assert source._is_file_path("test.txt") is True

    def test_is_file_path_with_path_object(self):
        """Test _is_file_path identifies Path objects correctly."""
        source = LineSource("test.txt")
        assert source._is_file_path(Path("test.txt")) is True

    def test_is_file_path_with_stringio(self):
        """Test _is_file_path rejects file-like objects."""
        source = LineSource("test.txt")
        assert source._is_file_path(StringIO("content")) is False

    def test_is_file_path_with_list(self):
        """Test _is_file_path rejects lists."""
        source = LineSource("test.txt")
        assert source._is_file_path(["line1", "line2"]) is False

    def test_is_file_like_with_stringio(self):
        """Test _is_file_like identifies StringIO correctly."""
        source = LineSource("test.txt")
        assert source._is_file_like(StringIO("content")) is True

    def test_is_file_like_with_open_file(self, temp_dir):
        """Test _is_file_like identifies open files correctly."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("content", encoding="utf-8")

        source = LineSource("test.txt")
        with open(file_path, "r", encoding="utf-8") as f:
            assert source._is_file_like(f) is True

    def test_is_file_like_with_string(self):
        """Test _is_file_like rejects strings."""
        source = LineSource("test.txt")
        assert source._is_file_like("test.txt") is False

    def test_is_file_like_with_list(self):
        """Test _is_file_like rejects lists."""
        source = LineSource("test.txt")
        assert source._is_file_like(["line1", "line2"]) is False

    def test_normalize_line_removes_newline(self):
        """Test _normalize_line removes newlines correctly."""
        source = LineSource([])
        assert source._normalize_line("line\n") == "line"

    def test_normalize_line_removes_carriage_return(self):
        """Test _normalize_line removes carriage returns correctly."""
        source = LineSource([])
        assert source._normalize_line("line\r") == "line"

    def test_normalize_line_removes_crlf(self):
        """Test _normalize_line removes CRLF correctly."""
        source = LineSource([])
        assert source._normalize_line("line\r\n") == "line"

    def test_normalize_line_no_modification_needed(self):
        """Test _normalize_line leaves clean lines unchanged."""
        source = LineSource([])
        assert source._normalize_line("line") == "line"


class TestLineSourceIntegration:
    """Integration tests combining multiple aspects."""

    def test_multiple_iterations_from_list(self):
        """Test that list source can be iterated multiple times."""
        lines_input = ["line1", "line2", "line3"]
        source = LineSource(lines_input)

        first_iteration = list(source)
        second_iteration = list(source)

        assert first_iteration == ["line1", "line2", "line3"]
        assert second_iteration == ["line1", "line2", "line3"]

    def test_multiple_iterations_from_file(self, temp_dir):
        """Test that file source can be iterated multiple times."""
        file_path = temp_dir / "test.txt"
        file_path.write_text("line1\nline2\nline3\n", encoding="utf-8")

        source = LineSource(str(file_path))

        first_iteration = list(source)
        second_iteration = list(source)

        assert first_iteration == ["line1", "line2", "line3"]
        assert second_iteration == ["line1", "line2", "line3"]

    def test_partial_iteration(self):
        """Test partial iteration using next()."""
        lines_input = ["line1", "line2", "line3", "line4"]
        source = LineSource(lines_input)

        iterator = iter(source)
        assert next(iterator) == "line1"
        assert next(iterator) == "line2"
        # Continue iteration
        remaining = list(iterator)
        assert remaining == ["line3", "line4"]

    def test_use_in_for_loop(self):
        """Test using LineSource in a for loop."""
        lines_input = ["line1\n", "line2\n", "line3\n"]
        source = LineSource(lines_input)

        result = []
        for line in source:
            result.append(line.upper())

        assert result == ["LINE1", "LINE2", "LINE3"]

    def test_use_with_list_comprehension(self):
        """Test using LineSource in list comprehension."""
        lines_input = ["line1\n", "line2\n", "line3\n"]
        source = LineSource(lines_input)

        result = [line.upper() for line in source]

        assert result == ["LINE1", "LINE2", "LINE3"]

    def test_real_csv_file_example(self, temp_dir):
        """Test with a realistic CSV file example."""
        file_path = temp_dir / "data.csv"
        csv_content = (
            "id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com\n"
        )
        file_path.write_text(csv_content, encoding="utf-8")

        source = LineSource(str(file_path))
        lines = list(source)

        assert len(lines) == 3
        assert lines[0] == "id,name,email"
        assert lines[1] == "1,Alice,alice@example.com"
        assert lines[2] == "2,Bob,bob@example.com"

    def test_chaining_with_filter(self):
        """Test chaining LineSource with filter operations."""
        lines_input = ["# comment\n", "data1\n", "# another comment\n", "data2\n"]
        source = LineSource(lines_input)

        non_comments = [line for line in source if not line.startswith("#")]

        assert non_comments == ["data1", "data2"]

    def test_empty_lines_preserved(self):
        """Test that empty lines are preserved in iteration."""
        lines_input = ["line1\n", "\n", "line3\n", "\n", "line5\n"]
        source = LineSource(lines_input)
        lines = list(source)

        assert lines == ["line1", "", "line3", "", "line5"]

    def test_large_file_streaming(self, temp_dir):
        """Test streaming from a larger file."""
        file_path = temp_dir / "large.txt"
        num_lines = 1000
        content = "\n".join([f"line {i}" for i in range(num_lines)]) + "\n"
        file_path.write_text(content, encoding="utf-8")

        source = LineSource(str(file_path))
        lines = list(source)

        assert len(lines) == num_lines
        assert lines[0] == "line 0"
        assert lines[-1] == "line 999"

    def test_mixed_source_types_same_behavior(self, temp_dir):
        """Test that different source types produce same results."""
        content_lines = ["line1", "line2", "line3"]

        # Test with list
        source_list = LineSource(content_lines)
        result_list = list(source_list)

        # Test with StringIO
        source_stringio = LineSource(StringIO("\n".join(content_lines) + "\n"))
        result_stringio = list(source_stringio)

        # Test with file
        file_path = temp_dir / "test.txt"
        file_path.write_text("\n".join(content_lines) + "\n", encoding="utf-8")
        source_file = LineSource(str(file_path))
        result_file = list(source_file)

        assert (
            result_list == result_stringio == result_file == ["line1", "line2", "line3"]
        )
