"""Managed email domains: SES identity lifecycle (mocked boto3) and quota gating."""
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import pytest

from src.core.exceptions import ValidationError
from src.domain.email_domains.service import (
    EmailDomainService,
    EmailQuotaService,
    build_dns_instructions,
)


def _settings(**overrides):
    base = dict(
        cloud_ses_region="us-east-1",
        cloud_ses_access_key_id="AKIA_TEST",
        cloud_ses_secret_access_key="secret",
        email_overage_rate_per_1000_usd_cents=30,
    )
    base.update(overrides)
    return SimpleNamespace(**base)


def _domain_repo(**method_overrides):
    repo = SimpleNamespace(
        get_by_domain=AsyncMock(return_value=None),
        get_by_workspace=AsyncMock(return_value=None),
        get_by_id=AsyncMock(return_value=None),
        create=AsyncMock(),
        mark_verified=AsyncMock(),
        mark_failed=AsyncMock(),
        set_mail_from=AsyncMock(),
        delete=AsyncMock(),
    )
    for key, value in method_overrides.items():
        setattr(repo, key, value)
    return repo


# ── build_dns_instructions ───────────────────────────────────────────────────────────────

def test_build_dns_instructions_dkim_only():
    instructions = build_dns_instructions("mail.example.com", ["tok1", "tok2", "tok3"], None, "us-east-1")
    assert len(instructions.records) == 3
    for i, rec in enumerate(instructions.records, start=1):
        assert rec.step == i
        assert rec.purpose == "dkim"
        assert rec.type == "CNAME"
        assert rec.host == f"tok{i}._domainkey.mail.example.com"
        assert rec.value == f"tok{i}.dkim.amazonses.com"


def test_build_dns_instructions_with_mail_from():
    instructions = build_dns_instructions(
        "mail.example.com", ["tok1", "tok2", "tok3"], "bounce.mail.example.com", "eu-west-1"
    )
    assert len(instructions.records) == 5
    mx = instructions.records[3]
    spf = instructions.records[4]
    assert mx.type == "MX"
    assert mx.value == "feedback-smtp.eu-west-1.amazonses.com"
    assert mx.priority == 10
    assert spf.type == "TXT"
    assert "amazonses.com" in spf.value


# ── EmailDomainService ───────────────────────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_add_domain_rejects_invalid_hostname():
    service = EmailDomainService(_domain_repo(), _settings())
    with pytest.raises(ValidationError):
        await service.add_domain(uuid4(), "not a domain")


@pytest.mark.asyncio
async def test_add_domain_rejects_duplicate_for_other_workspace():
    other_ws = uuid4()
    repo = _domain_repo(
        get_by_domain=AsyncMock(return_value=SimpleNamespace(workspace_id=other_ws)),
    )
    service = EmailDomainService(repo, _settings())
    with pytest.raises(ValidationError, match="already claimed"):
        await service.add_domain(uuid4(), "mail.example.com")


@pytest.mark.asyncio
async def test_add_domain_extracts_dkim_tokens_and_creates_config_set():
    ws = uuid4()
    created = SimpleNamespace(id=uuid4(), workspace_id=ws, domain="mail.example.com")
    repo = _domain_repo(create=AsyncMock(return_value=created))

    ses_client = MagicMock()
    ses_client.create_configuration_set.return_value = {}
    ses_client.create_email_identity.return_value = {
        "DkimAttributes": {"Tokens": ["t1", "t2", "t3"], "Status": "PENDING"}
    }

    service = EmailDomainService(repo, _settings(), ses_client_factory=lambda: ses_client)
    result = await service.add_domain(ws, "mail.example.com")

    assert result is created
    ses_client.create_configuration_set.assert_called_once_with(ConfigurationSetName=f"ws-{ws}")
    ses_client.create_email_identity.assert_called_once()
    _, kwargs = repo.create.call_args
    assert kwargs["dkim_tokens"] == ["t1", "t2", "t3"]
    assert kwargs["configuration_set_name"] == f"ws-{ws}"


@pytest.mark.asyncio
async def test_check_verification_marks_verified_on_success():
    d = SimpleNamespace(id=uuid4(), workspace_id=uuid4(), domain="mail.example.com", status="pending_dns")
    repo = _domain_repo(
        get_by_id=AsyncMock(return_value=d),
        mark_verified=AsyncMock(return_value=SimpleNamespace(**{**d.__dict__, "status": "verified"})),
    )
    ses_client = MagicMock()
    ses_client.get_email_identity.return_value = {
        "VerifiedForSendingStatus": True,
        "DkimAttributes": {"Status": "SUCCESS"},
    }
    service = EmailDomainService(repo, _settings(), ses_client_factory=lambda: ses_client)

    result = await service.check_verification(d.id)
    repo.mark_verified.assert_awaited_once_with(d.id)
    assert result.status == "verified"


@pytest.mark.asyncio
async def test_check_verification_marks_failed_on_dkim_failed():
    d = SimpleNamespace(id=uuid4(), domain="mail.example.com", status="pending_dns")
    repo = _domain_repo(get_by_id=AsyncMock(return_value=d))
    ses_client = MagicMock()
    ses_client.get_email_identity.return_value = {
        "VerifiedForSendingStatus": False,
        "DkimAttributes": {"Status": "FAILED"},
    }
    service = EmailDomainService(repo, _settings(), ses_client_factory=lambda: ses_client)

    await service.check_verification(d.id)
    repo.mark_failed.assert_awaited_once()


# ── EmailQuotaService ────────────────────────────────────────────────────────────────────

def _new_row_defaults(workspace_id, **kw):
    base = dict(
        workspace_id=workspace_id,
        consumed_monthly=0,
        consumed_today=0,
        pending_overage_units=0,
        override_included_monthly=None,
        override_daily_cap=None,
        override_rate_per_1000_minor=None,
    )
    base.update(kw)
    return SimpleNamespace(**base)


def _quota_repo(existing=None):
    return SimpleNamespace(
        get_for_update=AsyncMock(return_value=existing),
        get=AsyncMock(return_value=existing),
        create=AsyncMock(side_effect=_new_row_defaults),
        set_override=AsyncMock(),
    )


def _plan_quota(included=4000, daily_cap=130):
    return SimpleNamespace(
        email_included_monthly=AsyncMock(return_value=included),
        email_daily_cap=AsyncMock(return_value=daily_cap),
    )


def _sub_repo(sub=None):
    return SimpleNamespace(get_by_workspace=AsyncMock(return_value=sub))


def _quota_row(**overrides):
    now = datetime.now(timezone.utc)
    base = dict(
        workspace_id=uuid4(),
        period_start=now.replace(day=1, hour=0, minute=0, second=0, microsecond=0),
        period_end=(now.replace(day=1) + timedelta(days=32)).replace(day=1),
        included_monthly=4000,
        consumed_monthly=0,
        daily_cap=130,
        consumed_today=0,
        day_bucket_date=now.date(),
        pending_overage_units=0,
        override_included_monthly=None,
        override_daily_cap=None,
        override_rate_per_1000_minor=None,
    )
    base.update(overrides)
    return SimpleNamespace(**base)


@pytest.mark.asyncio
async def test_allows_send_within_monthly_and_daily_limits():
    row = _quota_row(consumed_monthly=10, consumed_today=5)
    svc = EmailQuotaService(_quota_repo(row), _plan_quota(), _sub_repo())
    decision = await svc.check_and_reserve(row.workspace_id)
    assert decision.allowed
    assert row.consumed_monthly == 11
    assert row.consumed_today == 6


@pytest.mark.asyncio
async def test_daily_cap_blocks_even_with_monthly_balance_remaining():
    row = _quota_row(consumed_monthly=10, consumed_today=130, daily_cap=130)
    svc = EmailQuotaService(_quota_repo(row), _plan_quota(), _sub_repo())
    decision = await svc.check_and_reserve(row.workspace_id)
    assert not decision.allowed
    assert decision.reason_code == "email_daily_cap_exceeded"
    assert row.consumed_monthly == 10  # unchanged


@pytest.mark.asyncio
async def test_monthly_exhaustion_falls_through_to_paid_overage_when_subscribed():
    row = _quota_row(consumed_monthly=4000, consumed_today=1)
    active_sub = SimpleNamespace(status="active", stripe_customer_id="cus_123")
    svc = EmailQuotaService(_quota_repo(row), _plan_quota(), _sub_repo(active_sub))
    decision = await svc.check_and_reserve(row.workspace_id)
    assert decision.allowed
    assert row.consumed_monthly == 4001
    assert row.pending_overage_units == 1


@pytest.mark.asyncio
async def test_monthly_exhaustion_hard_blocks_without_subscription():
    row = _quota_row(consumed_monthly=4000, consumed_today=1)
    svc = EmailQuotaService(_quota_repo(row), _plan_quota(), _sub_repo(None))
    decision = await svc.check_and_reserve(row.workspace_id)
    assert not decision.allowed
    assert decision.reason_code == "email_quota_exceeded_no_payment_method"
    assert row.pending_overage_units == 0


@pytest.mark.asyncio
async def test_day_rollover_resets_consumed_today():
    utc_today = datetime.now(timezone.utc).date()
    yesterday = utc_today - timedelta(days=1)
    row = _quota_row(consumed_today=130, daily_cap=130, day_bucket_date=yesterday, consumed_monthly=50)
    svc = EmailQuotaService(_quota_repo(row), _plan_quota(), _sub_repo())
    decision = await svc.check_and_reserve(row.workspace_id)
    assert decision.allowed
    assert row.consumed_today == 1
    assert row.day_bucket_date == utc_today


@pytest.mark.asyncio
async def test_override_takes_precedence_over_plan_defaults():
    row = _quota_row(consumed_monthly=4000, consumed_today=5, override_included_monthly=20000, override_daily_cap=1000)
    svc = EmailQuotaService(_quota_repo(row), _plan_quota(included=4000, daily_cap=130), _sub_repo())
    decision = await svc.check_and_reserve(row.workspace_id)
    assert decision.allowed  # would have been blocked on plan defaults alone
    assert row.consumed_monthly == 4001


@pytest.mark.asyncio
async def test_lazily_creates_quota_row_from_plan_on_first_use():
    ws = uuid4()
    repo = _quota_repo(existing=None)
    plan_quota = _plan_quota(included=10000, daily_cap=350)
    svc = EmailQuotaService(repo, plan_quota, _sub_repo())
    decision = await svc.check_and_reserve(ws)
    assert decision.allowed
    repo.create.assert_awaited_once()
    _, kwargs = repo.create.call_args
    assert kwargs["included_monthly"] == 10000
    assert kwargs["daily_cap"] == 350
