"""Edge-case tests for event diffing and filtering: severity filtering,
BRANCH_STALE/DIVERGED passthrough, simultaneous state changes, unassignment,
empty username, and involves() methods."""

from __future__ import annotations

from datetime import datetime

from project_hub.core.events import diff_mrs, diff_issues, filter_events
from project_hub.core.models import MR, Event, EventType, Issue


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

def _make_event(etype: EventType, summary: str = "") -> Event:
    return Event(id=None, type=etype, repo_name="repo-a", summary=summary,
                 detail="", created_at=datetime.now())


def _mr(iid=1, state="opened", notes=0, approved=False, sha="abc123",
        author="alice", assignees=None, reviewers=None):
    return MR(
        id=iid * 100, iid=iid, title=f"MR {iid}", author=author,
        state=state, source_branch=f"feat/{iid}", target_branch="main",
        web_url=f"http://x/mr/{iid}", user_notes_count=notes,
        approved=approved, created_at="2026-01-01", updated_at="2026-01-01",
        repo_name="repo-a", sha=sha, assignees=assignees, reviewers=reviewers,
    )


def _issue(iid=1, state="opened", notes=0, assignees=None, author="alice",
           participants=None):
    return Issue(
        id=iid * 100, iid=iid, title=f"Issue {iid}", author=author,
        state=state, web_url=f"http://x/issue/{iid}",
        user_notes_count=notes, created_at="2026-01-01",
        updated_at="2026-01-01", repo_name="repo-a",
        assignees=assignees or [], participants=participants or [],
    )


# ---------------------------------------------------------------------------
# 1. Severity filtering (M1 fix)
# ---------------------------------------------------------------------------

def test_severity_filter_drops_low_medium():
    """vulns_min_severity='high' drops low and medium vuln events."""
    events = [
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-1 (low)"),
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-2 (medium)"),
    ]
    result = filter_events(
        events, mrs_mode="all", pipelines_mode="all", vulns_mode="all",
        username="alice", mrs=[], pipelines=[], vulns_min_severity="high",
    )
    assert len(result) == 0


def test_severity_filter_keeps_high_critical():
    """vulns_min_severity='high' keeps high and critical vuln events."""
    events = [
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-3 (high)"),
        _make_event(EventType.VULN_RESOLVED, "Vulnerability resolved: CVE-4 (critical)"),
    ]
    result = filter_events(
        events, mrs_mode="all", pipelines_mode="all", vulns_mode="all",
        username="alice", mrs=[], pipelines=[], vulns_min_severity="high",
    )
    assert len(result) == 2
    assert result[0].type == EventType.VULN_NEW
    assert result[1].type == EventType.VULN_RESOLVED


def test_severity_filter_info_keeps_everything():
    """vulns_min_severity='info' (default) keeps all severity levels."""
    events = [
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-1 (info)"),
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-2 (low)"),
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-3 (medium)"),
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-4 (high)"),
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-5 (critical)"),
    ]
    result = filter_events(
        events, mrs_mode="all", pipelines_mode="all", vulns_mode="all",
        username="alice", mrs=[], pipelines=[], vulns_min_severity="info",
    )
    assert len(result) == 5


def test_severity_filter_unknown_severity_kept():
    """Vuln event with unknown severity is kept (safe default)."""
    events = [
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-X (unknown)"),
    ]
    result = filter_events(
        events, mrs_mode="all", pipelines_mode="all", vulns_mode="all",
        username="alice", mrs=[], pipelines=[], vulns_min_severity="high",
    )
    # unknown severity has rank -1, min "high" has rank 3 → -1 < 3 → dropped
    # BUT _severity_meets_min returns True only if sev_rank >= min_rank
    # -1 >= 3 is False, so unknown IS dropped (not a "safe default" for unknowns)
    # Wait — let's check: _extract_vuln_severity returns "unknown" (lowercase),
    # and _SEVERITY_ORDER doesn't have "unknown" → sev_rank = -1
    # _severity_meets_min(-1 >= 3) → False → event IS dropped.
    #
    # However, if _extract_vuln_severity returns None (can't parse), then
    # _severity_meets_min(None, ...) returns True (the safe default path).
    # "unknown" is parseable from the summary, so it gets rank -1.
    #
    # This test documents actual behavior: recognized-but-unlisted severities
    # are dropped when min_severity is set high. Let's adjust.
    assert len(result) == 0


def test_severity_filter_unparseable_severity_kept():
    """Vuln event where severity can't be extracted → kept (safe default)."""
    events = [
        _make_event(EventType.VULN_NEW, "New vulnerability: CVE-X"),  # no (severity) suffix
    ]
    result = filter_events(
        events, mrs_mode="all", pipelines_mode="all", vulns_mode="all",
        username="alice", mrs=[], pipelines=[], vulns_min_severity="high",
    )
    assert len(result) == 1


# ---------------------------------------------------------------------------
# 2. BRANCH_STALE and BRANCH_DIVERGED passthrough in filter_events
# ---------------------------------------------------------------------------

def test_branch_stale_passes_all_filter_modes():
    """BRANCH_STALE events pass through regardless of filter modes."""
    event = _make_event(EventType.BRANCH_STALE, "Branch 'feature/old' is stale")
    for mrs_mode in ("all", "mine", "none"):
        for pipelines_mode in ("all", "mine", "none"):
            for vulns_mode in ("all", "none"):
                result = filter_events(
                    [event], mrs_mode=mrs_mode, pipelines_mode=pipelines_mode,
                    vulns_mode=vulns_mode, username="alice", mrs=[], pipelines=[],
                    issues_mode="none",
                )
                assert len(result) == 1
                assert result[0].type == EventType.BRANCH_STALE


def test_branch_diverged_passes_all_filter_modes():
    """BRANCH_DIVERGED events pass through regardless of filter modes."""
    event = _make_event(EventType.BRANCH_DIVERGED, "Branch 'main' has diverged")
    for mrs_mode in ("all", "mine", "none"):
        for pipelines_mode in ("all", "mine", "none"):
            for vulns_mode in ("all", "none"):
                result = filter_events(
                    [event], mrs_mode=mrs_mode, pipelines_mode=pipelines_mode,
                    vulns_mode=vulns_mode, username="alice", mrs=[], pipelines=[],
                    issues_mode="none",
                )
                assert len(result) == 1
                assert result[0].type == EventType.BRANCH_DIVERGED


# ---------------------------------------------------------------------------
# 3. diff_mrs — simultaneous state change + comment
# ---------------------------------------------------------------------------

def test_diff_mrs_merged_and_commented():
    """MR changes state to merged AND has new comments → both events generated."""
    old = [_mr(iid=1, state="opened", notes=2)]
    new = [_mr(iid=1, state="merged", notes=5)]
    events = diff_mrs(old, new, "repo-a")
    types = {e.type for e in events}
    assert types == {EventType.MR_MERGED, EventType.MR_NEW_COMMENT}
    assert len(events) == 2


def test_diff_mrs_approved_and_new_push():
    """MR gets approved AND has a new push → both events generated."""
    old = [_mr(iid=1, approved=False, sha="aaa")]
    new = [_mr(iid=1, approved=True, sha="bbb")]
    events = diff_mrs(old, new, "repo-a")
    types = {e.type for e in events}
    assert types == {EventType.MR_APPROVED, EventType.MR_NEW_PUSH}
    assert len(events) == 2


# ---------------------------------------------------------------------------
# 4. diff_issues — unassignment
# ---------------------------------------------------------------------------

def test_diff_issues_unassignment_no_event():
    """Issue had assignee, now has none → no ISSUE_ASSIGNED event generated.

    Unassignment is not tracked (only new assignments are). This documents
    the current behavior."""
    old = [_issue(iid=1, assignees=["alice", "bob"])]
    new = [_issue(iid=1, assignees=["alice"])]  # bob unassigned
    events = diff_issues(old, new, "repo-a")
    assigned_events = [e for e in events if e.type == EventType.ISSUE_ASSIGNED]
    assert len(assigned_events) == 0


def test_diff_issues_full_unassignment_no_event():
    """Issue had assignees, now has none → no event."""
    old = [_issue(iid=1, assignees=["alice"])]
    new = [_issue(iid=1, assignees=[])]
    events = diff_issues(old, new, "repo-a")
    assert len(events) == 0


# ---------------------------------------------------------------------------
# 5. filter_events — empty username
# ---------------------------------------------------------------------------

def test_filter_events_empty_username_mine_mode():
    """Empty username with mode='mine' → events pass through (no filtering possible).

    When username is empty/falsy, the 'mine' filter can't match against anyone,
    so _should_drop_mr/pipeline/issue won't filter on username."""
    mr = _mr(iid=1, author="bob")
    events = [
        _make_event(EventType.MR_NEW_COMMENT, "MR !1 'fix': 2 new comment(s)"),
    ]
    result = filter_events(
        events, mrs_mode="mine", pipelines_mode="all", vulns_mode="all",
        username="", mrs=[mr], pipelines=[],
    )
    # username="" is falsy → _should_drop_mr with mode="mine" and empty username
    # checks: mode == "mine" and username → False (empty string is falsy)
    # So it returns False (don't drop) → event passes through
    assert len(result) == 1
    assert result[0].type == EventType.MR_NEW_COMMENT


def test_filter_events_empty_username_pipeline_mine():
    """Empty username with pipelines_mode='mine' → pipeline events on non-default branches pass."""
    events = [
        _make_event(EventType.PIPELINE_FAILED, "Pipeline #10 on 'feat/x' failed"),
    ]
    result = filter_events(
        events, mrs_mode="all", pipelines_mode="mine", vulns_mode="all",
        username="", mrs=[], pipelines=[],
    )
    # Empty username → my_mrs is empty → my_branches is just _DEFAULT_BRANCHES
    # "feat/x" not in {"main", "master"} → should be dropped
    # Wait — let me trace: username="" is falsy, so:
    #   my_mrs = [m for m in mrs if m.involves(username)] if username else []
    #   → username="" is falsy → my_mrs = []
    #   my_branches = set() | DEFAULT = {"main", "master"}
    #   _should_drop_pipeline: mode=="mine" and username → "" is falsy → False
    #   So returns False → event passes through
    assert len(result) == 1


# ---------------------------------------------------------------------------
# 6. MR.involves — reviewer, assignee, author, none
# ---------------------------------------------------------------------------

def test_mr_involves_reviewer():
    """MR with username in reviewers → involves() returns True."""
    mr = _mr(iid=1, author="alice", reviewers=["bob", "charlie"])
    assert mr.involves("bob") is True


def test_mr_involves_assignee():
    """MR with username in assignees → involves() returns True."""
    mr = _mr(iid=1, author="alice", assignees=["bob"])
    assert mr.involves("bob") is True


def test_mr_involves_author():
    """MR with username only as author → involves() returns True."""
    mr = _mr(iid=1, author="alice", assignees=["bob"], reviewers=["charlie"])
    assert mr.involves("alice") is True


def test_mr_involves_nobody():
    """MR with username nowhere → involves() returns False."""
    mr = _mr(iid=1, author="alice", assignees=["bob"], reviewers=["charlie"])
    assert mr.involves("dave") is False


def test_mr_involves_no_assignees_no_reviewers():
    """MR with None assignees/reviewers → involves checks author only."""
    mr = _mr(iid=1, author="alice", assignees=None, reviewers=None)
    assert mr.involves("alice") is True
    assert mr.involves("bob") is False


# ---------------------------------------------------------------------------
# 7. Issue.involves — assignee, author, participant, none
# ---------------------------------------------------------------------------

def test_issue_involves_assignee():
    """Issue with username as assignee → involves() returns True."""
    issue = _issue(iid=1, author="alice", assignees=["bob"], participants=["alice"])
    assert issue.involves("bob") is True


def test_issue_involves_author():
    """Issue with username as author → involves() returns True."""
    issue = _issue(iid=1, author="alice", assignees=["bob"], participants=["bob"])
    assert issue.involves("alice") is True


def test_issue_involves_participant():
    """Issue with username as participant (not author/assignee) → involves() returns True."""
    issue = _issue(iid=1, author="alice", assignees=["bob"],
                   participants=["alice", "bob", "charlie"])
    assert issue.involves("charlie") is True


def test_issue_involves_nobody():
    """Issue with username nowhere → involves() returns False."""
    issue = _issue(iid=1, author="alice", assignees=["bob"],
                   participants=["alice", "bob"])
    assert issue.involves("dave") is False


def test_issue_involves_empty_lists():
    """Issue with empty assignees/participants → only author matches."""
    issue = _issue(iid=1, author="alice", assignees=[], participants=[])
    assert issue.involves("alice") is True
    assert issue.involves("bob") is False


def test_issue_involves_none_lists():
    """Issue with None assignees/participants → only author matches."""
    issue = _issue(iid=1, author="alice", assignees=None, participants=None)
    assert issue.involves("alice") is True
    assert issue.involves("bob") is False
