"""Invite flow: model constraints, create/list/revoke endpoints, accept flow."""
import pytest
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.utils import timezone
from rest_framework.test import APIClient

from apps.companies.models import Organization, OrganizationMembership
from apps.core.models import Invitation, Permission, Role, UserRole
from apps.hr.models import Employee

User = get_user_model()


@pytest.fixture(autouse=True)
def clear_throttle_cache():
    # /api/invitations/accept/ shares the "login" ScopedRateThrottle (10/min
    # per anon IP) — clear between tests so this file's several accept calls
    # don't trip the real rate limit against each other.
    cache.clear()
    yield
    cache.clear()


def client_for(user):
    client = APIClient()
    client.force_authenticate(user)
    return client


@pytest.fixture
def org(db):
    return Organization.objects.create(name="Invite Test Org")


@pytest.fixture
def other_org(db):
    return Organization.objects.create(name="Invite Test Org B")


@pytest.fixture
def admin(db, org):
    """Superuser rewired into `org`, matching the established test pattern."""
    user = User.objects.create_superuser(email="invite-admin@test.local", password="p")
    auto_org = user.organizations.first()
    OrganizationMembership.objects.filter(organization=auto_org, user=user).update(organization=org)
    auto_org.delete()
    return user


@pytest.fixture
def staff(db, org):
    """Ordinary member of `org`, no rbac.manage permission."""
    user = User.objects.create_user(email="invite-staff@test.local", password="p")
    OrganizationMembership.objects.create(organization=org, user=user)
    return user


@pytest.fixture
def role(db):
    role = Role.objects.create(name="Invite Test Role", slug="invite-test-role")
    perm, _ = Permission.objects.get_or_create(domain="tasks", action="view", defaults={"label": "x"})
    role.permissions.add(perm)
    return role


@pytest.mark.django_db
class TestInvitationModel:
    def test_str(self, org):
        invite = Invitation.objects.create(organization=org, email="new@example.com")
        assert str(invite) == "new@example.com -> Invite Test Org (pending)"

    def test_expires_at_defaults_to_seven_days(self, org):
        invite = Invitation.objects.create(organization=org, email="new@example.com")
        delta = invite.expires_at - timezone.now()
        assert 6 <= delta.days <= 7

    def test_duplicate_pending_invite_same_org_email_rejected(self, org):
        Invitation.objects.create(organization=org, email="dup@example.com")
        with pytest.raises(Exception):
            Invitation.objects.create(organization=org, email="dup@example.com")

    def test_pending_and_revoked_can_coexist(self, org):
        first = Invitation.objects.create(organization=org, email="dup2@example.com")
        first.status = "revoked"
        first.save(update_fields=["status"])
        # A second pending invite for the same (org, email) is fine once the
        # first is no longer pending.
        Invitation.objects.create(organization=org, email="dup2@example.com")
        assert Invitation.objects.filter(organization=org, email="dup2@example.com").count() == 2


@pytest.mark.django_db
class TestInvitationCreateEndpoint:
    def test_admin_create_succeeds_and_returns_link(self, admin, org):
        resp = client_for(admin).post("/api/rbac/invitations/", {"email": "invitee@example.com"}, format="json")
        assert resp.status_code == 201, resp.content
        assert resp.data["email"] == "invitee@example.com"
        assert "token=" in resp.data["link"]
        invite = Invitation.objects.get(email="invitee@example.com")
        assert str(invite.organization_id).replace("-", "") == str(org.id).replace("-", "")
        assert invite.status == "pending"

    def test_non_admin_403s(self, staff):
        resp = client_for(staff).post("/api/rbac/invitations/", {"email": "invitee2@example.com"}, format="json")
        assert resp.status_code == 403

    def test_create_for_existing_member_email_400s(self, admin, staff):
        resp = client_for(admin).post("/api/rbac/invitations/", {"email": staff.email}, format="json")
        assert resp.status_code == 400
        assert not Invitation.objects.filter(email=staff.email).exists()

    def test_create_for_existing_user_not_yet_member_succeeds(self, admin, org):
        User.objects.create_user(email="platform-user@example.com", password="p")
        resp = client_for(admin).post("/api/rbac/invitations/", {"email": "platform-user@example.com"}, format="json")
        assert resp.status_code == 201, resp.content

    def test_reinvite_reuses_pending_row(self, admin, org, role):
        first = client_for(admin).post("/api/rbac/invitations/", {"email": "resend@example.com"}, format="json")
        assert first.status_code == 201
        first_id = first.data["id"]
        first_token = Invitation.objects.get(pk=first_id).token

        second = client_for(admin).post(
            "/api/rbac/invitations/", {"email": "resend@example.com", "role_id": role.id}, format="json",
        )
        assert second.status_code == 201
        assert second.data["id"] == first_id
        assert Invitation.objects.filter(email="resend@example.com").count() == 1
        refreshed = Invitation.objects.get(pk=first_id)
        assert refreshed.token != first_token
        assert refreshed.role_id == role.id


@pytest.mark.django_db
class TestInvitationAcceptEndpoint:
    def test_accept_new_email_creates_membership_role_and_logs_in(self, org, role):
        invite = Invitation.objects.create(organization=org, email="brandnew@example.com", role=role)
        client = APIClient()
        resp = client.post("/api/invitations/accept/", {
            "token": invite.token, "password": "supersecret123",
            "first_name": "New", "last_name": "Person",
        }, format="json")
        assert resp.status_code == 200, resp.content

        user = User.objects.get(email="brandnew@example.com")
        assert OrganizationMembership.objects.filter(organization=org, user=user).exists()
        assert UserRole.objects.filter(user=user, role=role, organization=org).exists()
        assert Employee.objects.filter(user=user).exists()
        invite.refresh_from_db()
        assert invite.status == "accepted"
        assert invite.accepted_at is not None

        # Logged in: an authenticated-only endpoint should now succeed on this session.
        me = client.get("/api/auth/me/")
        assert me.status_code == 200
        assert me.data["email"] == "brandnew@example.com"

    def test_accept_new_email_requires_password(self, org):
        invite = Invitation.objects.create(organization=org, email="nopass@example.com")
        resp = APIClient().post("/api/invitations/accept/", {"token": invite.token}, format="json")
        assert resp.status_code == 400

    def test_accept_existing_email_no_login_required(self, org, other_org):
        existing = User.objects.create_user(email="existing@example.com", password="p")
        OrganizationMembership.objects.create(organization=other_org, user=existing)
        invite = Invitation.objects.create(organization=org, email="existing@example.com")

        resp = APIClient().post("/api/invitations/accept/", {"token": invite.token}, format="json")
        assert resp.status_code == 200, resp.content
        assert OrganizationMembership.objects.filter(organization=org, user=existing).exists()

    def test_accept_expired_token_fails(self, org):
        invite = Invitation.objects.create(
            organization=org, email="expired@example.com",
            expires_at=timezone.now() - timezone.timedelta(days=1),
        )
        resp = APIClient().post("/api/invitations/accept/", {
            "token": invite.token, "password": "supersecret123",
        }, format="json")
        assert resp.status_code == 400

    def test_accept_already_accepted_token_fails(self, org):
        invite = Invitation.objects.create(organization=org, email="already@example.com", status="accepted")
        resp = APIClient().post("/api/invitations/accept/", {
            "token": invite.token, "password": "supersecret123",
        }, format="json")
        assert resp.status_code == 400

    def test_accept_wrong_token_fails(self, org):
        Invitation.objects.create(organization=org, email="whoever@example.com")
        resp = APIClient().post("/api/invitations/accept/", {
            "token": "not-a-real-token", "password": "supersecret123",
        }, format="json")
        assert resp.status_code == 400


@pytest.mark.django_db
class TestInvitationListAndRevoke:
    def test_list_scoped_to_active_org(self, admin, org, other_org):
        Invitation.objects.create(organization=org, email="mine@example.com")
        Invitation.objects.create(organization=other_org, email="theirs@example.com")

        resp = client_for(admin).get("/api/rbac/invitations/")
        assert resp.status_code == 200
        rows = resp.data["results"] if isinstance(resp.data, dict) and "results" in resp.data else resp.data
        emails = {r["email"] for r in rows}
        assert emails == {"mine@example.com"}

    def test_revoke_sets_status_and_404s_cross_org(self, admin, org, other_org):
        mine = Invitation.objects.create(organization=org, email="revoke-me@example.com")
        theirs = Invitation.objects.create(organization=other_org, email="not-mine@example.com")

        resp = client_for(admin).delete(f"/api/rbac/invitations/{mine.id}/")
        assert resp.status_code == 204
        mine.refresh_from_db()
        assert mine.status == "revoked"

        resp = client_for(admin).delete(f"/api/rbac/invitations/{theirs.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestAdminDirectCreateUser:
    """POST /api/rbac/users/ — direct add-user alternative to inviting."""

    def test_create_user_with_membership_roles_and_forced_password_change(self, admin, org):
        role = Role.objects.create(name="Direct Role", slug="direct-role")
        resp = client_for(admin).post("/api/rbac/users/", {
            "email": "Direct.Add@Example.com",
            "password": "changeme-now",
            "first_name": "Direct",
            "last_name": "Add",
            "role_ids": [role.id],
        }, format="json")

        assert resp.status_code == 201, resp.data
        user = User.objects.get(email="direct.add@example.com")  # normalized
        assert user.must_change_password is True
        assert user.check_password("changeme-now")
        assert OrganizationMembership.objects.filter(organization=org, user=user).exists()
        assert UserRole.objects.filter(user=user, role=role, organization=org).exists()
        assert Employee.objects.filter(user=user).exists()
        assert {r["name"] for r in resp.data["roles"]} == {"Direct Role"}

    def test_duplicate_email_rejected(self, admin, org):
        User.objects.create_user(email="taken@example.com", password="p12345678")
        resp = client_for(admin).post("/api/rbac/users/", {
            "email": "TAKEN@example.com", "password": "p12345678",
        }, format="json")
        assert resp.status_code == 400
        assert "already exists" in str(resp.data)

    def test_short_password_and_unknown_role_rejected(self, admin, org):
        resp = client_for(admin).post("/api/rbac/users/", {
            "email": "shortpw@example.com", "password": "short",
        }, format="json")
        assert resp.status_code == 400

        resp = client_for(admin).post("/api/rbac/users/", {
            "email": "badrole@example.com", "password": "p12345678", "role_ids": [999999],
        }, format="json")
        assert resp.status_code == 400
        assert not User.objects.filter(email="badrole@example.com").exists()

    def test_non_admin_cannot_create_users(self, org):
        member = User.objects.create_user(email="plain@example.com", password="p12345678")
        OrganizationMembership.objects.create(organization=org, user=member)
        resp = client_for(member).post("/api/rbac/users/", {
            "email": "sneaky@example.com", "password": "p12345678",
        }, format="json")
        assert resp.status_code == 403
