"""ITs for the @taxman ledger + Brief NVM-persistence wiring in the
my_sea views (user-spec 2026-05-26).

Covers:
- `my_sea_lock` first-card-of-cycle emits a TAX_LEDGER ledger entry +
  spawns a Brief + clears `User.free_draw_brief_dismissed_at`.
- `my_sea_paid_draw` commit emits a TAX_LEDGER ledger entry + spawns a
  Brief + clears `User.paid_draw_brief_dismissed_at`.
- `my_sea_dismiss_free_draw_brief` POST stamps the user's anchor; idem
  for `my_sea_dismiss_paid_draw_brief`.
- my_sea view context: `free_draw_brief_payload` / `paid_draw_brief_
  payload` populated only when the user's dismissal anchor is older than
  the latest TAX_LEDGER Brief of that slug-marker."""

import json
from datetime import timedelta

from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from apps.billboard.models import Brief, Line, Post
from apps.epic.models import personal_sig_cards
from apps.gameboard.models import MySeaDraw
from apps.lyric.models import Token, User


def _build_hand_payload(target_id, spread="situation-action-outcome"):
    """Single-card hand payload — enough for the FREE-DRAW first-card-of-
    cycle path, which is all we need to fire the tax debit."""
    return {
        "spread": spread,
        "hand": [{
            "position": "lay", "card_id": target_id,
            "reversed": False, "polarity": "gravity",
        }],
    }


class MySeaLockEmitsFreeDrawDebitTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="freedebit@test.io")
        self.client.force_login(self.user)
        self.target = personal_sig_cards(self.user)[0]
        self.user.significator = self.target
        self.user.save(update_fields=["significator"])

    def test_first_card_of_cycle_creates_tax_ledger_post_and_brief(self):
        # No TAX_LEDGER post initially
        self.assertEqual(
            Post.objects.filter(owner=self.user, kind=Post.KIND_TAX_LEDGER).count(), 0,
        )
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(self.target.id)),
            content_type="application/json",
        )
        post = Post.objects.get(owner=self.user, kind=Post.KIND_TAX_LEDGER)
        self.assertEqual(post.lines.count(), 1)
        self.assertIn("FREE DRAW", post.lines.first().text)
        brief = Brief.objects.get(owner=self.user, kind=Brief.KIND_TAX_LEDGER)
        self.assertEqual(brief.post_id, post.id)

    def test_first_card_of_cycle_clears_free_draw_dismissed_anchor(self):
        # Pre-stamp a dismissal anchor as though the user had NVM-ed a
        # prior cycle's Brief.
        self.user.free_draw_brief_dismissed_at = (
            timezone.now() - timedelta(days=2)
        )
        self.user.save(update_fields=["free_draw_brief_dismissed_at"])
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(self.target.id)),
            content_type="application/json",
        )
        self.user.refresh_from_db()
        self.assertIsNone(self.user.free_draw_brief_dismissed_at)

    def test_mid_cycle_lock_does_not_emit_extra_debit(self):
        # First-card-of-cycle is the ONLY moment a FREE DRAW debit fires.
        # A second /lock POST on an existing row (mid-draw upsert) must NOT
        # append another debit line — the cycle hasn't restarted.
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(self.target.id)),
            content_type="application/json",
        )
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(
                self.target.id, spread="situation-action-outcome",
            )),
            content_type="application/json",
        )
        post = Post.objects.get(owner=self.user, kind=Post.KIND_TAX_LEDGER)
        self.assertEqual(
            post.lines.count(), 1,
            "Mid-cycle upsert must not emit a second FREE DRAW debit",
        )


class MySeaPaidDrawEmitsDebitTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="paiddebit@test.io")
        self.client.force_login(self.user)
        self.target = personal_sig_cards(self.user)[0]
        self.user.significator = self.target
        # Pre-existing FREE-DRAW row w. a deposit reserved at the gate so
        # `my_sea_paid_draw` can commit it.
        self.user.last_free_draw_at = timezone.now() - timedelta(hours=2)
        self.user.save(update_fields=["significator", "last_free_draw_at"])
        token = Token.objects.create(user=self.user, token_type=Token.COIN)
        self.draw = MySeaDraw.objects.create(
            user=self.user,
            spread="situation-action-outcome",
            significator_id=self.target.id,
            hand=[],
            deposit_token_id=token.pk,
            deposit_reserved_at=timezone.now(),
        )

    def test_paid_draw_commit_creates_tax_ledger_debit_and_brief(self):
        self.client.post(reverse("my_sea_paid_draw"))
        post = Post.objects.get(owner=self.user, kind=Post.KIND_TAX_LEDGER)
        self.assertEqual(post.lines.count(), 1)
        self.assertIn("PAID DRAW", post.lines.first().text)
        self.assertEqual(
            Brief.objects.filter(owner=self.user, kind=Brief.KIND_TAX_LEDGER).count(),
            1,
        )

    def test_paid_draw_commit_clears_paid_draw_dismissed_anchor(self):
        self.user.paid_draw_brief_dismissed_at = (
            timezone.now() - timedelta(hours=3)
        )
        self.user.save(update_fields=["paid_draw_brief_dismissed_at"])
        self.client.post(reverse("my_sea_paid_draw"))
        self.user.refresh_from_db()
        self.assertIsNone(self.user.paid_draw_brief_dismissed_at)


class DismissBriefEndpointsTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="dismiss@test.io")
        self.client.force_login(self.user)

    def test_dismiss_free_draw_brief_stamps_user(self):
        self.assertIsNone(self.user.free_draw_brief_dismissed_at)
        before = timezone.now()
        response = self.client.post(reverse("my_sea_dismiss_free_draw_brief"))
        self.assertEqual(response.status_code, 204)
        self.user.refresh_from_db()
        self.assertIsNotNone(self.user.free_draw_brief_dismissed_at)
        self.assertGreaterEqual(self.user.free_draw_brief_dismissed_at, before)

    def test_dismiss_paid_draw_brief_stamps_user(self):
        before = timezone.now()
        response = self.client.post(reverse("my_sea_dismiss_paid_draw_brief"))
        self.assertEqual(response.status_code, 204)
        self.user.refresh_from_db()
        self.assertIsNotNone(self.user.paid_draw_brief_dismissed_at)
        self.assertGreaterEqual(self.user.paid_draw_brief_dismissed_at, before)

    def test_dismiss_endpoints_require_login(self):
        self.client.logout()
        for name in (
            "my_sea_dismiss_free_draw_brief",
            "my_sea_dismiss_paid_draw_brief",
        ):
            response = self.client.post(reverse(name))
            self.assertEqual(response.status_code, 302, name)

    def test_dismiss_endpoints_reject_get(self):
        for name in (
            "my_sea_dismiss_free_draw_brief",
            "my_sea_dismiss_paid_draw_brief",
        ):
            response = self.client.get(reverse(name))
            self.assertEqual(response.status_code, 405, name)


class MySeaContextBriefPayloadTest(TestCase):
    """The my_sea view exposes `free_draw_brief_payload` /
    `paid_draw_brief_payload` to the template — None when no
    eligible brief OR when dismissed anchor is recent enough."""

    def setUp(self):
        self.user = User.objects.create(email="payload@test.io")
        self.client.force_login(self.user)
        self.target = personal_sig_cards(self.user)[0]
        self.user.significator = self.target
        self.user.save(update_fields=["significator"])

    def test_payload_none_until_first_free_draw(self):
        response = self.client.get(reverse("my_sea"))
        self.assertIsNone(response.context["free_draw_brief_payload"])
        self.assertIsNone(response.context["paid_draw_brief_payload"])

    def test_free_draw_brief_payload_populated_after_first_card(self):
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(self.target.id)),
            content_type="application/json",
        )
        response = self.client.get(reverse("my_sea"))
        payload = response.context["free_draw_brief_payload"]
        self.assertIsNotNone(payload)
        self.assertIn("FREE DRAW", payload["line_text"])
        # dismiss_url is the new endpoint URL
        self.assertEqual(
            payload["dismiss_url"],
            reverse("my_sea_dismiss_free_draw_brief"),
        )

    def test_free_draw_brief_payload_suppressed_after_dismiss(self):
        # Spend a free draw, then dismiss the brief — subsequent GET
        # to /my-sea/ should omit the payload.
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(self.target.id)),
            content_type="application/json",
        )
        self.client.post(reverse("my_sea_dismiss_free_draw_brief"))
        response = self.client.get(reverse("my_sea"))
        self.assertIsNone(response.context["free_draw_brief_payload"])

    def test_payload_returns_after_fresh_cycle_resets_dismissal(self):
        # First cycle: spend + dismiss → suppressed.
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(self.target.id)),
            content_type="application/json",
        )
        self.client.post(reverse("my_sea_dismiss_free_draw_brief"))
        # Simulate 25h passing + the user starting a fresh FREE DRAW.
        # `_lock` will clear the dismissal anchor + emit a new debit.
        self.user.last_free_draw_at = timezone.now() - timedelta(hours=25)
        self.user.save(update_fields=["last_free_draw_at"])
        # The post-DEL flow leaves the row alive — clear hand so the next
        # /lock POST takes the first-card-of-cycle branch. Easiest: delete
        # the row to simulate cycle reset; the view then re-creates one.
        MySeaDraw.objects.filter(user=self.user).delete()
        self.client.post(
            reverse("my_sea_lock"),
            data=json.dumps(_build_hand_payload(self.target.id)),
            content_type="application/json",
        )
        response = self.client.get(reverse("my_sea"))
        # New brief emitted; dismissal anchor cleared → payload present.
        self.assertIsNotNone(response.context["free_draw_brief_payload"])
