"""
Note: some assertions are deliberately flexible to accommodate minor implementation
variations (e.g., unknown protocols or TCP story differences).
"""

from __future__ import annotations

import importlib
import math
import pkgutil
from collections.abc import Iterable

import pytest

import flownarrator as fn
from flownarrator.flow_sentences.numeric import safe_float, safe_int
from flownarrator.flow_sentences.protocol import (
    normalize_protocol,
    protocol_context,
)
from flownarrator.flow_sentences.quantitative import (
    bytes_phrase,
    duration_phrase,
    packets_phrase,
    rate_phrase,
)
from flownarrator.flow_sentences.roles import (
    dst_role_phrase,
    is_private_ip,
    src_role_phrase,
)
from flownarrator.flow_sentences.services import src_port_category
from flownarrator.flow_sentences.tcp import (
    handshake_story,
    normalize_flags,
    tcp_flags_story,
)

# ----------------------------------------------------------------------------
# Helpers
# ----------------------------------------------------------------------------

def _contains_any(text: str, keywords: Iterable[str]) -> bool:
    t = text.lower()
    return any(k.lower() in t for k in keywords)


# ----------------------------------------------------------------------------
# Numeric helpers
# ----------------------------------------------------------------------------

def test_numeric_safe_helpers_basic():
    """Verify normal parsing and default fallback behavior."""
    assert safe_int("3") == 3
    assert safe_int("x", 5) == 5
    assert safe_float("1.5") == 1.5
    assert safe_float("bad", 0) == 0


@pytest.mark.parametrize(
    "val, default",
    [
        (float("inf"), 7),
        (float("-inf"), 3.14),
        (float("nan"), 0.5),
        (str(math.inf), 9),
        ("NaN", 1.23),
    ],
)
def test_safe_float_handles_inf_nan(val, default):
    assert safe_float(val, default) == default


# ----------------------------------------------------------------------------
# Protocol mapping & context
# ----------------------------------------------------------------------------

def test_protocol_and_context_smoke():
    assert normalize_protocol(6) == "TCP"
    ctx = protocol_context("TCP")
    assert {"noun", "article"} <= set(ctx)
    assert isinstance(ctx["noun"], str) and isinstance(ctx["article"], str)


def test_normalize_protocol_uncommon_and_unknown():
    # Explicit mappings present in implementation
    assert normalize_protocol(47) == "GRE"
    assert normalize_protocol(132) == "SCTP"

    # Unknown numeric protocols: may echo number or map to OTHER
    assert normalize_protocol("999") in {"999", "OTHER"}

    # Unknown non-numeric values: uppercased token or OTHER
    assert normalize_protocol("weird") in {"WEIRD", "OTHER"}


def test_protocol_context_unknown_other_paths():
    ctx = protocol_context("OTHER")
    assert "noun" in ctx and isinstance(ctx["noun"], str)
    assert "article" in ctx and isinstance(ctx["article"], str)


# ----------------------------------------------------------------------------
# TCP flags & handshake stories
# ----------------------------------------------------------------------------

def test_tcp_flags_normalization_and_story_variants():
    ordered, _ = normalize_flags("SYN, ACK, FIN, PSH")
    assert ordered[0] == "SYN"

    for flags in (None, "", "ECE,CWR", "SYN,ACK", "FIN,ACK", "RST", "PSH", "URG", "ACK", "XYZ"):
        assert isinstance(tcp_flags_story(flags), str)

    for hs in (None, "", "handshake completed", "HANDSHAKE PARTIAL", "no handshake", "error", "X"):
        assert isinstance(handshake_story(hs), str)


# ----------------------------------------------------------------------------
# Import smoke coverage
# ----------------------------------------------------------------------------

def test_import_every_module():
    """Import all modules under `flownarrator` to ensure smoke coverage."""
    import flownarrator

    for mod in pkgutil.walk_packages(flownarrator.__path__, flownarrator.__name__ + "."):
        importlib.import_module(mod.name)


# ----------------------------------------------------------------------------
# Public API
# ----------------------------------------------------------------------------

def test_api_basic(minimal_df):
    out = fn.generate_sentences(minimal_df, show_progress=False)
    assert {"sentence", "response", "label"} <= set(out.columns)
    assert len(out) == len(minimal_df)


# ----------------------------------------------------------------------------
# Quantitative phrasing
# ----------------------------------------------------------------------------

@pytest.mark.parametrize(
    "sec, token",
    [
        (None, "invalid"),
        (-1, "invalid"),
        (0.0, "almost instantly"),
        (0.2, "fraction of a second"),
        (2.0, "only a few seconds"),
        (8.0, "lasting several seconds"),
        (45.0, "close to a minute"),
        (180.0, "for minutes"),
        (3600.0, "for a long time"),
    ],
)
def test_duration_phrase_full(sec, token):
    # Loose prefix check to handle minor textual variations
    assert token.split()[0] in duration_phrase(sec)


@pytest.mark.parametrize(
    "b, token",
    [
        (None, "corrupted"),
        (-1, "corrupted"),
        (0, "without"),
        (30, "handful"),
        (300, "small"),
        (1500, "moderate"),
        (7000, "significant"),
        (15000, "heavy"),
    ],
)
def test_bytes_phrase_full(b, token):
    assert token in bytes_phrase(b)


@pytest.mark.parametrize(
    "cnt, token",
    [
        (None, "invalid"),
        (-1, "invalid"),
        (0, "no packets"),
        (2, "couple"),
        (8, "handful"),
        (50, "dozens"),
        (400, "hundreds"),
        (5000, "flood"),
    ],
)
def test_packets_phrase_full(cnt, token):
    assert token in packets_phrase(cnt)


@pytest.mark.parametrize(
    "br, pr, token",
    [
        (0.0, 0.0, "very slow"),
        (3_000_000.0, 0.0, "very intense"),
        (600_000.0, 25.0, "fast"),
        (60_000.0, 5.0, "steady"),
        (6_000.0, 2.0, "slow"),
    ],
)
def test_rate_phrase_full(br, pr, token):
    assert token in rate_phrase(br, pr)


# ----------------------------------------------------------------------------
# Roles & services
# ----------------------------------------------------------------------------

def test_is_private_ip_ranges_and_invalid():
    # 10/8, 172.16/12, 192.168/16
    assert is_private_ip("10.0.0.1")
    assert is_private_ip("172.16.0.5")
    assert is_private_ip("192.168.1.7")

    # public vs loopback
    assert not is_private_ip("8.8.8.8")
    assert is_private_ip("127.0.0.1")

    # invalid input should fallback to False
    assert not is_private_ip("not_an_ip")


def test_role_phrases_cover_else_paths():
    assert "internal" in src_role_phrase("10.0.0.2")
    assert "Internet" in dst_role_phrase("8.8.4.4")


def test_src_port_category_edges():
    assert "system" in src_port_category(0)
    assert "well-known" in src_port_category(80)
    assert "registered" in src_port_category(1024)
    assert "ephemeral" in src_port_category(65535)

    # invalid cases
    assert "invalid" in src_port_category(-5)
    assert "invalid" in src_port_category(100_000)
