"""M6 autopass (§4.4): the fleshout scenario (zeina -> adi auto -> rian),
the one-hop guarantee (a second rule must NOT fire on the auto hop),
trigger variants, deactivation, the final-holder-only notification, the
serve and spike execution sites, and the owner-scoped GET/PUT API with
its §4.3b validation."""
from __future__ import annotations

from sqlalchemy import select

from app.models import AutopassRule, Ball, Notification, Touch
from tests.conftest import make_fleshout_match, mint_client_for


def _rule(db, world, owner, trigger, target, trigger_player=None, active=True):
    p = world["players"]
    rule = AutopassRule(
        match_id=world["match"].id,
        owner_player_id=p[owner].id,
        trigger=trigger,
        trigger_player_id=p[trigger_player].id if trigger_player else None,
        target_player_id=p[target].id,
        active=active,
    )
    db.add(rule)
    db.commit()
    return rule


def _ball(db, world, **overrides):
    fields = dict(
        court_id=world["court"].id,
        title="Autopass ball",
        status="Open",
        category="Revision",
        action_to=[],
        author_id=world["players"]["erin"].id,
    )
    fields.update(overrides)
    ball = Ball(**fields)
    db.add(ball)
    db.commit()
    return ball


def _touches(db, ball_id):
    return db.execute(
        select(Touch).where(Touch.ball_id == ball_id).order_by(Touch.seq)
    ).scalars().all()


def test_fleshout_scenario_pass_to_boundary_fires_rule(client, db_session):
    """Zeina passes over her net to Adi; Adi's from_subteam rule carries
    the ball onward to Rian in the same breath — two touches, the second
    kind "auto" with via_rule_id, action_to landing on Rian."""
    world = make_fleshout_match(db_session)
    p = world["players"]
    rule = _rule(db_session, world, "adi", "from_subteam", "rian")
    ball = _ball(db_session, world, action_to=[p["zeina"].id])

    zeina = mint_client_for(client, "zeina")
    resp = zeina.post(f"/api/balls/{ball.id}/pass", json={"to_players": [str(p["adi"].id)]})
    assert resp.status_code == 200
    assert resp.json()["action_to"] == [str(p["rian"].id)]

    touches = _touches(db_session, ball.id)
    assert [(t.kind, t.via_rule_id) for t in touches] == [("pass", None), ("auto", rule.id)]
    assert touches[1].from_player == p["adi"].id  # the rule's owner moved it
    assert touches[1].to_players == [p["rian"].id]

    # Final holder gets the ping; the auto-passed intermediate does not.
    recipients = db_session.execute(select(Notification.recipient_id)).scalars().all()
    assert recipients == [p["rian"].id]


def test_one_hop_guarantee(client, db_session):
    """Rian ALSO has a rule that matches the auto hop (from_any) — it must
    NOT fire: an auto touch never triggers another rule."""
    world = make_fleshout_match(db_session)
    p = world["players"]
    _rule(db_session, world, "adi", "from_subteam", "rian")
    _rule(db_session, world, "rian", "from_any", "erin")
    ball = _ball(db_session, world, action_to=[p["zeina"].id])

    zeina = mint_client_for(client, "zeina")
    resp = zeina.post(f"/api/balls/{ball.id}/pass", json={"to_players": [str(p["adi"].id)]})
    assert resp.status_code == 200
    assert resp.json()["action_to"] == [str(p["rian"].id)]  # stopped at rian
    assert [t.kind for t in _touches(db_session, ball.id)] == ["pass", "auto"]


def test_from_subteam_does_not_fire_on_front_line_pass(client, db_session):
    """Rian passes to Adi from the shared front line: Rian is not Adi's
    sub-team, so Adi's rule stays quiet."""
    world = make_fleshout_match(db_session)
    p = world["players"]
    _rule(db_session, world, "adi", "from_subteam", "rian")
    ball = _ball(db_session, world, action_to=[p["rian"].id])

    rian = mint_client_for(client, "rian")
    resp = rian.post(f"/api/balls/{ball.id}/pass", json={"to_players": [str(p["adi"].id)]})
    assert resp.status_code == 200
    assert resp.json()["action_to"] == [str(p["adi"].id)]
    assert [t.kind for t in _touches(db_session, ball.id)] == ["pass"]


def test_from_player_trigger_matches_only_that_player(client, db_session):
    world = make_fleshout_match(db_session)
    p = world["players"]
    _rule(db_session, world, "adi", "from_player", "zeina", trigger_player="rian")
    ball = _ball(db_session, world, action_to=[p["rahman"].id])

    # Rahman -> Adi: trigger watches rian, so nothing fires.
    rahman = mint_client_for(client, "rahman")
    resp = rahman.post(f"/api/balls/{ball.id}/pass", json={"to_players": [str(p["adi"].id)]})
    assert resp.json()["action_to"] == [str(p["adi"].id)]

    # Rian -> Adi: fires, onward to Zeina.
    rian = mint_client_for(client, "rian")
    resp = rian.post(f"/api/balls/{ball.id}/pass", json={"to_players": [str(p["adi"].id)]})
    assert resp.json()["action_to"] == [str(p["zeina"].id)]
    assert [t.kind for t in _touches(db_session, ball.id)] == ["pass", "pass", "auto"]


def test_inactive_rule_never_fires(client, db_session):
    world = make_fleshout_match(db_session)
    p = world["players"]
    _rule(db_session, world, "adi", "from_subteam", "rian", active=False)
    ball = _ball(db_session, world, action_to=[p["zeina"].id])

    zeina = mint_client_for(client, "zeina")
    resp = zeina.post(f"/api/balls/{ball.id}/pass", json={"to_players": [str(p["adi"].id)]})
    assert resp.json()["action_to"] == [str(p["adi"].id)]
    assert [t.kind for t in _touches(db_session, ball.id)] == ["pass"]


def test_serve_landing_on_receiver_fires_rule(client, db_session):
    """The fleshout composition at the SERVE: Zeina serves over her net
    to Adi (the boundary), Adi's rule carries it to Rian."""
    world = make_fleshout_match(db_session)
    p = world["players"]
    _rule(db_session, world, "adi", "from_subteam", "rian")

    zeina = mint_client_for(client, "zeina")
    resp = zeina.post(
        "/api/matches/fleshout/balls",
        json={"court_id": str(world["court"].id), "title": "Over the net",
              "action_to": [str(p["adi"].id)]},
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["action_to"] == [str(p["rian"].id)]
    touches = _touches(db_session, body["id"])
    assert [t.kind for t in touches] == ["serve", "auto"]
    assert touches[0].to_players == [p["adi"].id]  # the serve's raw truth
    # Only the final holder is pinged.
    recipients = db_session.execute(select(Notification.recipient_id)).scalars().all()
    assert recipients == [p["rian"].id]


def test_spike_to_author_fires_authors_rule(client, db_session):
    """The spiked-to author may autopass onward: Adi flips Ready for
    Review on Rian's ball; Rian's from_any rule hands it to Erin."""
    world = make_fleshout_match(db_session)
    p = world["players"]
    _rule(db_session, world, "rian", "from_any", "erin")
    ball = _ball(db_session, world, status="Addressing",
                 action_to=[p["adi"].id], author_id=p["rian"].id)

    adi = mint_client_for(client, "adi")
    resp = adi.patch(f"/api/balls/{ball.id}", json={"status": "Ready for Review"})
    assert resp.status_code == 200
    assert resp.json()["action_to"] == [str(p["erin"].id)]
    assert [t.kind for t in _touches(db_session, ball.id)] == ["spike", "auto"]
    # prev_action_to (rally memory) still holds the pre-spike hands.
    assert resp.json()["prev_action_to"] == [str(p["adi"].id)]


# ------------------------------------------------------------------- API


def test_autopass_api_upsert_and_owner_scope(client, db_session):
    world = make_fleshout_match(db_session)
    p = world["players"]

    adi = mint_client_for(client, "adi")
    resp = adi.put(
        "/api/matches/fleshout/autopass",
        json={"trigger": "from_subteam", "target_player_id": str(p["rian"].id)},
    )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["trigger"] == "from_subteam"
    assert body["target_player_id"] == str(p["rian"].id)
    assert body["active"] is True

    # Upsert: a second PUT replaces the ONE rule, never adds another.
    resp = adi.put(
        "/api/matches/fleshout/autopass",
        json={"trigger": "from_any", "target_player_id": str(p["rian"].id),
              "active": False},
    )
    assert resp.status_code == 200
    rules = adi.get("/api/matches/fleshout/autopass").json()["rules"]
    assert len(rules) == 1
    assert rules[0]["trigger"] == "from_any"
    assert rules[0]["active"] is False

    # Owner-scoped: another player's GET shows THEIR rules (none).
    zeina = mint_client_for(client, "zeina")
    assert zeina.get("/api/matches/fleshout/autopass").json()["rules"] == []


def test_autopass_api_validation(client, db_session):
    world = make_fleshout_match(db_session)
    p = world["players"]
    erin = mint_client_for(client, "erin")

    resp = erin.put(
        "/api/matches/fleshout/autopass",
        json={"trigger": "whenever", "target_player_id": str(p["rian"].id)},
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "INVALID_TRIGGER"

    resp = erin.put(
        "/api/matches/fleshout/autopass",
        json={"trigger": "from_player", "target_player_id": str(p["rian"].id)},
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "TRIGGER_PLAYER_REQUIRED"

    # §4.3b: automation is a standing pass — the target must be visible.
    resp = erin.put(
        "/api/matches/fleshout/autopass",
        json={"trigger": "from_any", "target_player_id": str(p["zeina"].id)},
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "TARGET_NOT_VISIBLE"

    resp = erin.put(
        "/api/matches/fleshout/autopass",
        json={"trigger": "from_any", "target_player_id": str(p["erin"].id)},
    )
    assert resp.status_code == 422
    assert resp.json()["error_code"] == "INVALID_AUTOPASS_TARGET"

    # A clean rule passes; a from_any upsert clears any stray trigger_player.
    resp = erin.put(
        "/api/matches/fleshout/autopass",
        json={"trigger": "from_any", "target_player_id": str(p["rob"].id),
              "trigger_player_id": str(p["tracy"].id)},
    )
    assert resp.status_code == 200
    assert resp.json()["trigger_player_id"] is None
