"""
Tests for LineAccumulator — Milestone 1.

Test categories:
    1. Basic line reassembly (CRLF, LF, bare CR)
    2. Chunk splitting (lines split across multiple feed() calls)
    3. Pagination erase (--More-- pattern)
    4. Edge cases (empty input, very long lines, binary garbage)
    5. Pending buffer for prompt detection
    6. Line counter accuracy
"""

import pytest
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sniffer.accumulator import LineAccumulator


# ── Basic line endings ──────────────────────────────────────────

class TestBasicLineEndings:
    """Standard line terminator handling."""

    def test_crlf(self):
        """Standard \\r\\n produces one line."""
        acc = LineAccumulator()
        lines = acc.feed("show ip route\r\n")
        assert lines == ["show ip route"]

    def test_lf(self):
        """Bare \\n produces one line."""
        acc = LineAccumulator()
        lines = acc.feed("show ip route\n")
        assert lines == ["show ip route"]

    def test_multiple_crlf_lines(self):
        """Multiple lines in one chunk."""
        acc = LineAccumulator()
        lines = acc.feed("line one\r\nline two\r\nline three\r\n")
        assert lines == ["line one", "line two", "line three"]

    def test_multiple_lf_lines(self):
        """Multiple bare-LF lines in one chunk."""
        acc = LineAccumulator()
        lines = acc.feed("line one\nline two\nline three\n")
        assert lines == ["line one", "line two", "line three"]

    def test_mixed_endings(self):
        """Mix of \\r\\n and \\n in one chunk."""
        acc = LineAccumulator()
        lines = acc.feed("crlf line\r\nlf line\nanother\r\n")
        assert lines == ["crlf line", "lf line", "another"]

    def test_empty_line_crlf(self):
        """\\r\\n alone produces an empty string line."""
        acc = LineAccumulator()
        lines = acc.feed("\r\n")
        assert lines == [""]

    def test_empty_line_lf(self):
        """\\n alone produces an empty string line."""
        acc = LineAccumulator()
        lines = acc.feed("\n")
        assert lines == [""]

    def test_consecutive_empty_lines(self):
        """Multiple blank lines."""
        acc = LineAccumulator()
        lines = acc.feed("\r\n\r\n\r\n")
        assert lines == ["", "", ""]


# ── Chunk splitting ─────────────────────────────────────────────

class TestChunkSplitting:
    """Lines split across multiple feed() calls."""

    def test_line_split_in_half(self):
        """A line arrives in two chunks."""
        acc = LineAccumulator()
        assert acc.feed("show ip ") == []
        assert acc.feed("route\r\n") == ["show ip route"]

    def test_line_split_at_cr(self):
        """Chunk boundary falls between \\r and \\n."""
        acc = LineAccumulator()
        assert acc.feed("some output\r") == []
        assert acc.feed("\n") == ["some output"]

    def test_three_chunks(self):
        """A line arrives in three pieces."""
        acc = LineAccumulator()
        assert acc.feed("show ") == []
        assert acc.feed("ip bgp") == []
        assert acc.feed(" summary\r\n") == ["show ip bgp summary"]

    def test_arbitrary_split_points(self):
        """Split a known multi-line string at every possible position."""
        full = "line one\r\nline two\r\nline three\r\n"
        expected = ["line one", "line two", "line three"]

        for split_at in range(1, len(full)):
            acc = LineAccumulator()
            chunk1 = full[:split_at]
            chunk2 = full[split_at:]
            lines = acc.feed(chunk1) + acc.feed(chunk2)
            assert lines == expected, f"Failed splitting at position {split_at}"

    def test_single_char_chunks(self):
        """Feed one character at a time."""
        acc = LineAccumulator()
        full = "hello\r\nworld\r\n"
        all_lines = []
        for ch in full:
            all_lines.extend(acc.feed(ch))
        assert all_lines == ["hello", "world"]

    def test_multiple_lines_one_chunk_plus_partial(self):
        """Two complete lines + start of a third."""
        acc = LineAccumulator()
        lines = acc.feed("line1\r\nline2\r\npartial")
        assert lines == ["line1", "line2"]
        assert acc.pending == "partial"
        lines = acc.feed(" finish\r\n")
        assert lines == ["partial finish"]


# ── Carriage return / line overwrite ────────────────────────────

class TestCarriageReturn:
    """Bare \\r (without \\n) handling — the pagination erase pattern."""

    def test_cr_overwrites_buffer(self):
        """\\r followed by new text replaces the buffer."""
        acc = LineAccumulator()
        lines = acc.feed("loading...\rComplete\r\n")
        assert lines == ["Complete"]

    def test_more_prompt_erase(self):
        """Classic --More-- erase sequence."""
        acc = LineAccumulator()
        # Device sends --More--, user presses space,
        # device sends \r + spaces + \r + next content
        lines = acc.feed("--More--")
        assert lines == []
        assert acc.pending == "--More--"

        lines = acc.feed("\r         \r")
        assert lines == []
        # Buffer has been reset by the second \r
        assert acc.pending == ""

        lines = acc.feed("Next page line 1\r\n")
        assert lines == ["Next page line 1"]

    def test_more_erase_single_chunk(self):
        """--More-- erase in one chunk."""
        acc = LineAccumulator()
        lines = acc.feed("--More--\r         \rNext page\r\n")
        assert lines == ["Next page"]

    def test_progress_indicator(self):
        """Spinner-style progress: repeated \\r overwrites."""
        acc = LineAccumulator()
        acc.feed("Progress: 10%")
        acc.feed("\rProgress: 50%")
        acc.feed("\rProgress: 100%")
        lines = acc.feed("\r\n")
        assert lines == ["Progress: 100%"]

    def test_cr_at_chunk_boundary_then_newline(self):
        """\\r at end of chunk, \\n at start of next → \\r\\n pair."""
        acc = LineAccumulator()
        acc.feed("output text\r")
        lines = acc.feed("\n")
        assert lines == ["output text"]

    def test_cr_at_chunk_boundary_then_text(self):
        """\\r at end of chunk, text at start of next → overwrite."""
        acc = LineAccumulator()
        acc.feed("old content\r")
        lines = acc.feed("new content\r\n")
        assert lines == ["new content"]

    def test_double_cr(self):
        """Double \\r resets twice (common in pagination)."""
        acc = LineAccumulator()
        lines = acc.feed("junk\r\r")
        assert lines == []
        assert acc.pending == ""

    def test_cr_only_no_following(self):
        """Bare \\r with nothing after — buffer cleared, stash holds old content."""
        acc = LineAccumulator()
        lines = acc.feed("waiting\r")
        assert lines == []
        # Buffer cleared immediately on \r — pending is empty.
        # Old content stashed internally for \r\n recovery.
        assert acc.pending == ""


# ── Edge cases ──────────────────────────────────────────────────

class TestEdgeCases:
    """Boundary conditions and unusual input."""

    def test_empty_input(self):
        """Empty string returns empty list."""
        acc = LineAccumulator()
        assert acc.feed("") == []

    def test_none_like_empty(self):
        """Empty string after real data."""
        acc = LineAccumulator()
        acc.feed("partial")
        assert acc.feed("") == []
        assert acc.pending == "partial"

    def test_very_long_line(self):
        """No max line length — very long lines accumulate."""
        acc = LineAccumulator()
        long_text = "x" * 100_000
        assert acc.feed(long_text) == []
        assert len(acc.pending) == 100_000
        lines = acc.feed("\r\n")
        assert len(lines) == 1
        assert len(lines[0]) == 100_000

    def test_binary_garbage(self):
        """Non-printable characters pass through without crashing."""
        acc = LineAccumulator()
        garbage = "".join(chr(i) for i in range(32, 127))  # printable ASCII
        lines = acc.feed(garbage + "\r\n")
        assert lines == [garbage]

    def test_unicode(self):
        """UTF-8 content passes through."""
        acc = LineAccumulator()
        lines = acc.feed("日本語テスト\r\n")
        assert lines == ["日本語テスト"]

    def test_only_terminators(self):
        """String of only \\r\\n pairs."""
        acc = LineAccumulator()
        lines = acc.feed("\r\n\r\n")
        assert lines == ["", ""]

    def test_only_lf(self):
        """String of only \\n."""
        acc = LineAccumulator()
        lines = acc.feed("\n\n\n")
        assert lines == ["", "", ""]


# ── Pending buffer ──────────────────────────────────────────────

class TestPendingBuffer:
    """The pending property for prompt detection."""

    def test_pending_after_partial(self):
        """Incomplete line sits in pending."""
        acc = LineAccumulator()
        acc.feed("router#")
        assert acc.pending == "router#"

    def test_pending_empty_after_complete(self):
        """Pending is empty after a complete line."""
        acc = LineAccumulator()
        acc.feed("some output\r\n")
        assert acc.pending == ""

    def test_pending_updates_across_chunks(self):
        """Pending grows as partial chunks arrive."""
        acc = LineAccumulator()
        acc.feed("rou")
        assert acc.pending == "rou"
        acc.feed("ter#")
        assert acc.pending == "router#"

    def test_pending_resets_after_line_emit(self):
        """Pending reflects only content after the last emitted line."""
        acc = LineAccumulator()
        acc.feed("first line\r\nsecond_partial")
        assert acc.pending == "second_partial"

    def test_pending_with_prompt_after_output(self):
        """Typical session: output lines then a prompt sitting in buffer."""
        acc = LineAccumulator()
        lines = acc.feed("BGP summary info\r\nNeighbor  AS  State\r\n10.0.0.1  65000  Estab\r\nwan-rtr#")
        assert lines == ["BGP summary info", "Neighbor  AS  State", "10.0.0.1  65000  Estab"]
        assert acc.pending == "wan-rtr#"


# ── Line counter ────────────────────────────────────────────────

class TestLineCounter:
    """Line count tracking for block offset references."""

    def test_counter_starts_at_zero(self):
        acc = LineAccumulator()
        assert acc.line_count == 0

    def test_counter_increments(self):
        acc = LineAccumulator()
        acc.feed("line 1\r\n")
        assert acc.line_count == 1
        acc.feed("line 2\r\nline 3\r\n")
        assert acc.line_count == 3

    def test_counter_with_split_chunks(self):
        acc = LineAccumulator()
        acc.feed("partial ")
        assert acc.line_count == 0
        acc.feed("line\r\n")
        assert acc.line_count == 1

    def test_counter_counts_empty_lines(self):
        acc = LineAccumulator()
        acc.feed("\r\n\r\n\r\n")
        assert acc.line_count == 3

    def test_counter_not_affected_by_cr(self):
        """Bare \\r does NOT emit a line, so counter stays."""
        acc = LineAccumulator()
        acc.feed("overwrite\rthis\rcount\r\n")
        assert acc.line_count == 1


# ── Reset ───────────────────────────────────────────────────────

class TestReset:
    def test_reset_clears_state(self):
        acc = LineAccumulator()
        acc.feed("some content\r\nmore\r\n")
        acc.feed("partial")
        assert acc.line_count == 2
        assert acc.pending == "partial"

        acc.reset()
        assert acc.line_count == 0
        assert acc.pending == ""

    def test_works_after_reset(self):
        acc = LineAccumulator()
        acc.feed("old\r\n")
        acc.reset()
        lines = acc.feed("new\r\n")
        assert lines == ["new"]
        assert acc.line_count == 1


# ── Realistic device output ────────────────────────────────────

class TestRealisticOutput:
    """Simulated real device output patterns."""

    def test_arista_show_ip_bgp_summary(self):
        """Arista EOS show ip bgp summary with prompt."""
        output = (
            "BGP summary information for VRF default\r\n"
            "Router identifier 172.16.1.2, local AS number 65000\r\n"
            "Neighbor Status Codes: m - Under maintenance\r\n"
            "  Neighbor     V AS      MsgRcvd MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc\r\n"
            "  10.0.0.1     4 65000    125678   98432    0    0 3d12h04m Estab   450    448\r\n"
            "  10.0.0.5     4 65001     45678   34567    0    0 1d06h30m Estab   220    220\r\n"
            "wan-rtr#"
        )
        acc = LineAccumulator()
        lines = acc.feed(output)
        assert len(lines) == 6
        assert lines[0] == "BGP summary information for VRF default"
        assert "10.0.0.1" in lines[4]
        assert acc.pending == "wan-rtr#"
        assert acc.line_count == 6

    def test_paginated_show_run(self):
        """Paginated output with --More-- erase across chunks."""
        acc = LineAccumulator()

        # First page
        acc.feed("hostname wan-rtr\r\n")
        acc.feed("!\r\n")
        acc.feed("interface Ethernet1\r\n")

        # --More-- prompt (no newline — sits in pending)
        acc.feed(" --More-- ")
        assert acc.pending == " --More-- "

        # User presses space, device erases and sends next page
        lines = acc.feed("\r          \r")
        assert lines == []  # No complete line emitted
        assert acc.pending == ""  # Buffer reset by pagination erase

        # Next page content
        lines = acc.feed("   description uplink\r\n")
        assert lines == ["   description uplink"]

        assert acc.line_count == 4  # 3 from first page + 1 from second

    def test_login_banner_then_prompt(self):
        """MOTD banner followed by a prompt sitting in buffer."""
        acc = LineAccumulator()
        banner = (
            "\r\n"
            "****************************************\r\n"
            "* Authorized access only               *\r\n"
            "* All activity is monitored and logged  *\r\n"
            "****************************************\r\n"
            "\r\n"
        )
        lines = acc.feed(banner)
        assert len(lines) == 6  # empty, 4 banner lines, empty

        # Prompt arrives without newline
        acc.feed("wan-rtr#")
        assert acc.pending == "wan-rtr#"

    def test_command_echo_and_output(self):
        """User types command, device echoes it, then output + prompt."""
        acc = LineAccumulator()

        # Command echo (user typed, device echoes with \r\n)
        lines = acc.feed("show version\r\n")
        assert lines == ["show version"]

        # Output
        output = (
            "Arista vEOS-lab\r\n"
            "Hardware version: \r\n"
            "Serial number: SN-12345\r\n"
        )
        lines = acc.feed(output)
        assert len(lines) == 3

        # Prompt
        acc.feed("wan-rtr#")
        assert acc.pending == "wan-rtr#"
        assert acc.line_count == 4

    def test_juniper_style_pagination(self):
        """Juniper uses 'lines X-Y' style pagination."""
        acc = LineAccumulator()
        acc.feed("output line 1\r\n")
        acc.feed("output line 2\r\n")
        # Juniper pagination indicator (no newline)
        acc.feed("---( more )---")
        assert acc.pending == "---( more )---"
        # User presses space, device erases
        acc.feed("\r              \r")
        assert acc.pending == ""
        acc.feed("output line 3\r\n")
        assert acc.line_count == 3


if __name__ == "__main__":
    pytest.main([__file__, "-v"])