# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for :mod:`data.mesh_ingestor.connection`."""

from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import patch

import pytest

REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))

from data.mesh_ingestor.connection import (  # noqa: E402
    BLE_ADDRESS_RE,
    DEFAULT_TCP_PORT,
    default_serial_targets,
    parse_ble_target,
    parse_tcp_target,
)

# ---------------------------------------------------------------------------
# parse_ble_target
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "value,expected",
    [
        # MAC addresses — returned upper-cased
        ("AA:BB:CC:DD:EE:FF", "AA:BB:CC:DD:EE:FF"),
        ("aa:bb:cc:dd:ee:ff", "AA:BB:CC:DD:EE:FF"),
        ("AA:BB:CC:DD:EE:12", "AA:BB:CC:DD:EE:12"),
        # UUID (macOS format)
        (
            "12345678-1234-1234-1234-123456789abc",
            "12345678-1234-1234-1234-123456789ABC",
        ),
        (
            "12345678-1234-1234-1234-123456789ABC",
            "12345678-1234-1234-1234-123456789ABC",
        ),
    ],
)
def test_parse_ble_target_accepts_ble_addresses(value, expected):
    """parse_ble_target must return the normalised address for valid BLE formats."""
    assert parse_ble_target(value) == expected


@pytest.mark.parametrize(
    "value",
    [
        "/dev/ttyUSB0",
        "/dev/ttyACM0",
        "COM3",
        "hostname:4403",
        "192.168.1.1:4403",
        "",
        "   ",
        "AA:BB:CC:DD:EE",  # too short — only 5 groups
        "ZZ:BB:CC:DD:EE:FF",  # invalid hex
    ],
)
def test_parse_ble_target_rejects_non_ble(value):
    """parse_ble_target must return None for serial paths, TCP targets, and malformed inputs."""
    assert parse_ble_target(value) is None


def test_parse_ble_target_none_input():
    """parse_ble_target must return None for None input."""
    assert parse_ble_target(None) is None  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# parse_tcp_target
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "value,expected_host,expected_port",
    [
        # hostname:port
        ("meshcore-node.local:4403", "meshcore-node.local", 4403),
        ("meshnode.local:4403", "meshnode.local", 4403),
        ("hostname:1234", "hostname", 1234),
        ("otherhost:80", "otherhost", 80),
        # IP:port
        ("192.168.1.1:4403", "192.168.1.1", 4403),
        ("10.0.0.1:9000", "10.0.0.1", 9000),
        # With scheme prefix
        ("tcp://meshnode.local:4403", "meshnode.local", 4403),
        ("http://192.168.1.1:4403", "192.168.1.1", 4403),
        # IPv6 with brackets
        ("[::1]:4403", "::1", 4403),
        ("[2001:db8::1]:8080", "2001:db8::1", 8080),
    ],
)
def test_parse_tcp_target_accepts_tcp(value, expected_host, expected_port):
    """parse_tcp_target must return (host, port) for valid TCP target strings."""
    result = parse_tcp_target(value)
    assert result is not None
    host, port = result
    assert host == expected_host
    assert port == expected_port


@pytest.mark.parametrize(
    "value",
    [
        # Serial paths
        "/dev/ttyUSB0",
        "/dev/ttyACM0",
        "COM3",
        # BLE MACs — multiple colons, no valid port
        "AA:BB:CC:DD:EE:FF",
        "AA:BB:CC:DD:EE:12",
        # UUIDs — hyphens, no colon
        "12345678-1234-1234-1234-123456789abc",
        # Bare hostname without port
        "meshcore-node.local",
        # Empty / whitespace
        "",
        "   ",
        # Port out of range
        "host:0",
        "host:65536",
        # Non-numeric port
        "host:notaport",
    ],
)
def test_parse_tcp_target_rejects_non_tcp(value):
    """parse_tcp_target must return None for serial paths, BLE addresses, and malformed inputs."""
    assert parse_tcp_target(value) is None


def test_parse_tcp_target_none_input():
    """parse_tcp_target must return None for None input."""
    assert parse_tcp_target(None) is None  # type: ignore[arg-type]


def test_parse_tcp_target_default_port_for_bracketed_ipv6_no_port():
    """parse_tcp_target must use DEFAULT_TCP_PORT for bracketed IPv6 without port."""
    result = parse_tcp_target("[::1]")
    assert result == ("::1", DEFAULT_TCP_PORT)


@pytest.mark.parametrize(
    "value",
    [
        "[::1",  # no closing bracket
        "[]:4403",  # empty host in brackets
        "[::1]:abc",  # non-numeric port after bracket
        "[::1]:0",  # port out of range (low)
        "[::1]:65536",  # port out of range (high)
    ],
)
def test_parse_tcp_target_rejects_malformed_ipv6(value):
    """parse_tcp_target must return None for malformed bracketed IPv6 targets."""
    assert parse_tcp_target(value) is None


# ---------------------------------------------------------------------------
# default_serial_targets
# ---------------------------------------------------------------------------


def test_default_serial_targets_returns_list():
    """default_serial_targets must return a non-empty list."""
    targets = default_serial_targets()
    assert isinstance(targets, list)
    assert len(targets) > 0


def test_default_serial_targets_includes_fallback():
    """default_serial_targets always includes /dev/ttyACM0 as a fallback."""
    targets = default_serial_targets()
    assert "/dev/ttyACM0" in targets


def test_default_serial_targets_no_duplicates():
    """default_serial_targets must not return duplicate paths."""
    targets = default_serial_targets()
    assert len(targets) == len(set(targets))


def test_default_serial_targets_deduplicates_glob_results():
    """default_serial_targets must deduplicate paths returned by multiple globs."""

    def _fake_glob(pattern):
        if "ttyACM" in pattern:
            return ["/dev/ttyACM0", "/dev/ttyACM1"]
        if "ttyUSB" in pattern:
            return ["/dev/ttyACM0"]  # intentional duplicate across patterns
        return []

    with patch("data.mesh_ingestor.connection.glob.glob", side_effect=_fake_glob):
        targets = default_serial_targets()

    assert targets.count("/dev/ttyACM0") == 1
    assert "/dev/ttyACM1" in targets
    # ttyACM0 already found by glob so fallback append must not re-add it
    assert targets.count("/dev/ttyACM0") == 1


def test_default_serial_targets_omits_fallback_when_ttyacm0_found():
    """default_serial_targets must not append /dev/ttyACM0 when glob already found it."""

    def _fake_glob(pattern):
        if "ttyACM" in pattern:
            return ["/dev/ttyACM0"]
        return []

    with patch("data.mesh_ingestor.connection.glob.glob", side_effect=_fake_glob):
        targets = default_serial_targets()

    # present exactly once — from glob, not appended again
    assert targets.count("/dev/ttyACM0") == 1


# ---------------------------------------------------------------------------
# BLE_ADDRESS_RE sanity
# ---------------------------------------------------------------------------


def test_ble_address_re_mac():
    """BLE_ADDRESS_RE matches a canonical 6-byte MAC address."""
    assert BLE_ADDRESS_RE.fullmatch("AA:BB:CC:DD:EE:FF") is not None


def test_ble_address_re_uuid():
    """BLE_ADDRESS_RE matches a standard 128-bit UUID."""
    assert BLE_ADDRESS_RE.fullmatch("12345678-1234-1234-1234-123456789abc") is not None


def test_ble_address_re_rejects_tcp():
    """BLE_ADDRESS_RE must not match a hostname:port string."""
    assert BLE_ADDRESS_RE.fullmatch("hostname:4403") is None


def test_ble_address_re_rejects_partial_mac():
    """BLE_ADDRESS_RE must not match an incomplete MAC address."""
    assert BLE_ADDRESS_RE.fullmatch("AA:BB:CC:DD:EE") is None
