"""
Phase 0 tests: guest mode, new calculator variables, heat score, quote flow, migration.
"""
import math

import pytest
from padelnomics.leads.routes import calculate_heat_score
from padelnomics.planner.calculator import DEFAULTS, calc, validate_state

ALL_COMBOS = [
    ("indoor", "rent"),
    ("indoor", "buy"),
    ("outdoor", "rent"),
    ("outdoor", "buy"),
]


def default_state(**overrides):
    s = {**DEFAULTS, **overrides}
    return validate_state(s)


def _assert_finite(obj, path=""):
    if isinstance(obj, float):
        assert math.isfinite(obj), f"Non-finite at {path}: {obj}"
    elif isinstance(obj, dict):
        for k, v in obj.items():
            _assert_finite(v, f"{path}.{k}")
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            _assert_finite(v, f"{path}[{i}]")


# ════════════════════════════════════════════════════════════
# Guest mode
# ════════════════════════════════════════════════════════════

class TestGuestMode:
    async def test_planner_accessible_without_login(self, client):
        """GET /planner/ returns 200 for unauthenticated user."""
        resp = await client.get("/en/planner/")
        assert resp.status_code == 200

    async def test_calculate_endpoint_works_without_login(self, client):
        """POST /planner/calculate returns HTML partial for guest."""
        resp = await client.post(
            "/en/planner/calculate",
            data={"dblCourts": "4", "activeTab": "capex"},
        )
        assert resp.status_code == 200
        html = (await resp.data).decode()
        # HTMX endpoint returns an HTML partial containing CAPEX data
        assert "capex" in html.lower() or "metric-card" in html

    async def test_scenario_routes_require_login(self, client):
        """Save/load/delete/list scenarios still require auth."""
        resp = await client.post(
            "/en/planner/scenarios/save",
            json={"name": "test", "state_json": "{}"},
        )
        assert resp.status_code in (302, 401)

    async def test_planner_hides_save_for_guest(self, client):
        """Planner HTML does not render scenario controls for guests."""
        resp = await client.get("/en/planner/")
        html = (await resp.data).decode()
        assert "saveScenarioBtn" not in html

    async def test_planner_shows_save_for_auth(self, auth_client):
        """Planner HTML renders scenario controls for logged-in users."""
        resp = await auth_client.get("/en/planner/")
        html = (await resp.data).decode()
        assert "saveScenarioBtn" in html


# ════════════════════════════════════════════════════════════
# New calculator variables — defaults
# ════════════════════════════════════════════════════════════

class TestNewCalculatorVariables:
    def test_budget_target_in_defaults(self):
        assert "budgetTarget" in DEFAULTS
        assert DEFAULTS["budgetTarget"] == 0

    def test_glass_type_in_defaults(self):
        assert "glassType" in DEFAULTS
        assert DEFAULTS["glassType"] == "standard"

    def test_lighting_type_in_defaults(self):
        assert "lightingType" in DEFAULTS
        assert DEFAULTS["lightingType"] == "led_standard"


# ════════════════════════════════════════════════════════════
# Glass type
# ════════════════════════════════════════════════════════════

class TestGlassType:
    def test_panoramic_glass_increases_capex(self):
        d_std = calc(default_state(glassType="standard"))
        d_pan = calc(default_state(glassType="panoramic"))
        assert d_pan["capex"] > d_std["capex"]

    @pytest.mark.parametrize("glass", ["standard", "panoramic"])
    @pytest.mark.parametrize("venue,own", ALL_COMBOS)
    def test_glass_type_all_combos(self, venue, own, glass):
        d = calc(default_state(venue=venue, own=own, glassType=glass))
        assert d["capex"] > 0
        _assert_finite(d)

    def test_panoramic_applies_1_4x_multiplier(self):
        """Panoramic courts cost 1.4x standard courts."""
        d_std = calc(default_state(glassType="standard"))
        d_pan = calc(default_state(glassType="panoramic"))
        std_courts = next(i for i in d_std["capexItems"] if i["name"] == "Padel Courts")
        pan_courts = next(i for i in d_pan["capexItems"] if i["name"] == "Padel Courts")
        assert pan_courts["amount"] == pytest.approx(std_courts["amount"] * 1.4, abs=1)


# ════════════════════════════════════════════════════════════
# Lighting type
# ════════════════════════════════════════════════════════════

class TestLightingType:
    def test_led_competition_increases_capex(self):
        d_std = calc(default_state(lightingType="led_standard"))
        d_comp = calc(default_state(lightingType="led_competition"))
        assert d_comp["capex"] > d_std["capex"]

    def test_natural_light_outdoor(self):
        d_led = calc(default_state(venue="outdoor", lightingType="led_standard"))
        d_nat = calc(default_state(venue="outdoor", lightingType="natural"))
        assert d_nat["capex"] < d_led["capex"]

    def test_natural_light_zeroes_outdoor_lighting(self):
        d = calc(default_state(venue="outdoor", lightingType="natural"))
        lighting_item = next(
            (i for i in d["capexItems"] if i["name"] == "Lighting"), None
        )
        assert lighting_item is not None
        assert lighting_item["amount"] == 0

    @pytest.mark.parametrize("light", ["led_standard", "led_competition"])
    @pytest.mark.parametrize("venue,own", ALL_COMBOS)
    def test_lighting_type_all_combos(self, venue, own, light):
        d = calc(default_state(venue=venue, own=own, lightingType=light))
        assert d["capex"] > 0
        _assert_finite(d)


# ════════════════════════════════════════════════════════════
# Budget target
# ════════════════════════════════════════════════════════════

class TestBudgetTarget:
    def test_budget_variance_when_set(self):
        d = calc(default_state(budgetTarget=300000))
        assert "budgetVariance" in d
        assert d["budgetVariance"] == d["capex"] - 300000

    def test_budget_variance_zero_when_no_budget(self):
        d = calc(default_state(budgetTarget=0))
        assert d["budgetVariance"] == 0
        assert d["budgetPct"] == 0

    def test_budget_pct_calculated(self):
        d = calc(default_state(budgetTarget=200000))
        assert d["budgetPct"] == pytest.approx(d["capex"] / 200000 * 100)

    def test_budget_target_passthrough(self):
        d = calc(default_state(budgetTarget=500000))
        assert d["budgetTarget"] == 500000


# ════════════════════════════════════════════════════════════
# Heat score
# ════════════════════════════════════════════════════════════

class TestHeatScore:
    def test_hot_lead(self):
        """High-readiness signals = hot."""
        form = {
            "timeline": "asap",
            "location_status": "lease_signed",
            "financing_status": "self_funded",
            "decision_process": "solo",
            "previous_supplier_contact": "received_quotes",
            "budget_estimate": "500000",
        }
        assert calculate_heat_score(form) == "hot"

    def test_cool_lead(self):
        """Low-readiness signals = cool."""
        form = {
            "timeline": "12+mo",
            "location_status": "still_searching",
            "financing_status": "not_started",
            "decision_process": "committee",
            "previous_supplier_contact": "first_time",
            "budget_estimate": "0",
        }
        assert calculate_heat_score(form) == "cool"

    def test_warm_lead(self):
        """Mid-readiness signals = warm."""
        form = {
            "timeline": "3-6mo",
            "location_status": "location_found",
            "financing_status": "seeking",
            "decision_process": "partners",
            "budget_estimate": "150000",
        }
        assert calculate_heat_score(form) == "warm"

    def test_empty_form_is_cool(self):
        assert calculate_heat_score({}) == "cool"

    def test_timeline_6_12mo_scores_1(self):
        assert calculate_heat_score({"timeline": "6-12mo"}) == "cool"

    def test_high_budget_alone_not_hot(self):
        """Budget alone shouldn't make a lead hot."""
        assert calculate_heat_score({"budget_estimate": "1000000"}) == "cool"

    def test_permit_granted_scores_4(self):
        assert calculate_heat_score({"location_status": "permit_granted"}) == "cool"
        # Combined with timeline makes it warm
        form = {"location_status": "permit_granted", "timeline": "asap"}
        assert calculate_heat_score(form) == "warm"

    def test_permit_pending_scores_3(self):
        form = {
            "location_status": "permit_pending",
            "timeline": "3-6mo",
            "financing_status": "self_funded",
        }
        assert calculate_heat_score(form) == "warm"

    def test_converting_existing_scores_2(self):
        assert calculate_heat_score({"location_status": "converting_existing"}) == "cool"

    def test_permit_not_filed_scores_2(self):
        assert calculate_heat_score({"location_status": "permit_not_filed"}) == "cool"

    def test_location_found_scores_1(self):
        assert calculate_heat_score({"location_status": "location_found"}) == "cool"


# ════════════════════════════════════════════════════════════
# Quote request route
# ════════════════════════════════════════════════════════════

class TestQuoteRequest:
    async def test_quote_form_loads(self, client):
        """GET /leads/quote returns 200 with wizard shell."""
        resp = await client.get("/en/leads/quote")
        assert resp.status_code == 200

    async def test_quote_prefill_from_params(self, client):
        """Query params pre-fill the form and start on step 2."""
        resp = await client.get("/en/leads/quote?venue=outdoor&courts=6")
        assert resp.status_code == 200

    async def test_quote_step_endpoint(self, client):
        """GET /leads/quote/step/1 returns 200 partial."""
        resp = await client.get("/en/leads/quote/step/1")
        assert resp.status_code == 200

    async def test_quote_step_invalid(self, client):
        """GET /leads/quote/step/0 returns 400."""
        resp = await client.get("/en/leads/quote/step/0")
        assert resp.status_code == 400

    async def test_quote_step_post_advances(self, client):
        """POST to step 1 with valid data returns step 2."""
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await client.post(
            "/en/leads/quote/step/1",
            form={
                "_accumulated": "{}",
                "facility_type": "indoor",
                "court_count": "4",
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200
        html = (await resp.data).decode()
        assert "Location" in html

    async def test_quote_submit_creates_lead(self, client, db):
        """POST /leads/quote creates a lead_requests row as pending_verification."""
        # Get CSRF token first
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await client.post(
            "/en/leads/quote",
            form={
                "facility_type": "indoor",
                "court_count": "4",
                "glass_type": "panoramic",
                "lighting_type": "led_standard",
                "build_context": "new_standalone",
                "country": "DE",
                "timeline": "3-6mo",
                "location_status": "location_found",
                "financing_status": "self_funded",
                "decision_process": "solo",
                "stakeholder_type": "entrepreneur",
                "contact_name": "Test User",
                "contact_email": "test@example.com",
                "contact_phone": "+491234567890",
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200

        async with db.execute("SELECT * FROM lead_requests WHERE lead_type = 'quote'") as cur:
            rows = await cur.fetchall()
        assert len(rows) == 1
        row = dict(rows[0])
        assert row["status"] == "pending_verification"
        assert row["heat_score"] in ("hot", "warm", "cool")
        assert row["contact_email"] == "test@example.com"
        assert row["facility_type"] == "indoor"
        assert row["stakeholder_type"] == "entrepreneur"

    async def test_quote_submit_without_login(self, client, db):
        """Guests get a user created and linked; lead is pending_verification."""
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await client.post(
            "/en/leads/quote",
            form={
                "facility_type": "indoor",
                "court_count": "2",
                "country": "DE",
                "timeline": "3-6mo",
                "stakeholder_type": "entrepreneur",
                "contact_name": "Guest",
                "contact_email": "guest@example.com",
                "contact_phone": "+491234567890",
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200

        async with db.execute(
            "SELECT user_id, status FROM lead_requests WHERE contact_email = 'guest@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row is not None
        assert row[0] is not None  # user_id linked via get-or-create
        assert row[1] == "pending_verification"

    async def test_quote_submit_with_login(self, auth_client, db, test_user):
        """Logged-in user with matching email skips verification (status='new')."""
        await auth_client.get("/en/leads/quote")
        async with auth_client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await auth_client.post(
            "/en/leads/quote",
            form={
                "facility_type": "outdoor",
                "court_count": "6",
                "country": "DE",
                "timeline": "asap",
                "stakeholder_type": "entrepreneur",
                "contact_name": "Auth User",
                "contact_email": "test@example.com",  # matches test_user email
                "contact_phone": "+491234567890",
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200

        async with db.execute(
            "SELECT user_id, status FROM lead_requests WHERE contact_email = 'test@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row is not None
        assert row[0] == test_user["id"]
        assert row[1] == "new"

    async def test_venue_search_build_context(self, client, db):
        """Build context 'venue_search' is stored correctly."""
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await client.post(
            "/en/leads/quote",
            form={
                "facility_type": "indoor",
                "court_count": "4",
                "build_context": "venue_search",
                "country": "DE",
                "timeline": "6-12mo",
                "stakeholder_type": "developer",
                "contact_name": "Venue Search",
                "contact_email": "venue@example.com",
                "contact_phone": "+491234567890",
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200

        async with db.execute(
            "SELECT build_context FROM lead_requests WHERE contact_email = 'venue@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row is not None
        assert row[0] == "venue_search"

    async def test_stakeholder_type_stored(self, client, db):
        """stakeholder_type field is stored correctly."""
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await client.post(
            "/en/leads/quote",
            form={
                "facility_type": "indoor",
                "court_count": "6",
                "country": "DE",
                "timeline": "asap",
                "stakeholder_type": "tennis_club",
                "contact_name": "Club Owner",
                "contact_email": "club@example.com",
                "contact_phone": "+491234567890",
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200

        async with db.execute(
            "SELECT stakeholder_type FROM lead_requests WHERE contact_email = 'club@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row is not None
        assert row[0] == "tennis_club"

    async def test_submitted_page_has_context(self, client):
        """Guest quote submission shows 'check your email' verify page."""
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await client.post(
            "/en/leads/quote",
            form={
                "facility_type": "indoor",
                "court_count": "6",
                "country": "DE",
                "timeline": "3-6mo",
                "stakeholder_type": "entrepreneur",
                "contact_name": "Context Test",
                "contact_email": "ctx@example.com",
                "contact_phone": "+491234567890",
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200
        html = (await resp.data).decode()
        assert "check your email" in html.lower()
        assert "ctx@example.com" in html

    async def test_quote_validation_rejects_missing_fields(self, client):
        """POST /leads/quote returns 422 JSON when mandatory fields missing."""
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await client.post(
            "/en/leads/quote",
            json={
                "facility_type": "indoor",
                "court_count": "4",
                "contact_name": "",
                "contact_email": "",
            },
            headers={"X-CSRF-Token": csrf},
        )
        assert resp.status_code == 422
        data = await resp.get_json()
        assert data["ok"] is False
        assert len(data["errors"]) >= 3  # country, timeline, stakeholder_type + name + email + phone


# ════════════════════════════════════════════════════════════
# Quote verification (double opt-in)
# ════════════════════════════════════════════════════════════

class TestQuoteVerification:
    """Double opt-in email verification for quote requests."""

    QUOTE_FORM = {
        "facility_type": "indoor",
        "court_count": "4",
        "country": "DE",
        "timeline": "3-6mo",
        "stakeholder_type": "entrepreneur",
        "contact_name": "Verify Test",
        "contact_email": "verify@example.com",
        "contact_phone": "+491234567890",
    }

    async def _submit_guest_quote(self, client, db, email="verify@example.com"):
        """Helper: submit a quote as a guest, return (lead_id, auth_token, lead_token)."""
        await client.get("/en/leads/quote")
        async with client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")
        form = {**self.QUOTE_FORM, "contact_email": email, "csrf_token": csrf}
        await client.post("/en/leads/quote", form=form)

        async with db.execute(
            "SELECT id, token FROM lead_requests WHERE contact_email = ?", (email,)
        ) as cur:
            row = await cur.fetchone()
            lead_id, lead_token = row[0], row[1]

        async with db.execute(
            "SELECT token FROM auth_tokens ORDER BY id DESC LIMIT 1"
        ) as cur:
            auth_token = (await cur.fetchone())[0]

        return lead_id, auth_token, lead_token

    async def test_guest_quote_creates_pending_lead(self, client, db):
        """Guest quote creates lead with status='pending_verification'."""
        lead_id, _, _lt = await self._submit_guest_quote(client, db)

        async with db.execute(
            "SELECT status FROM lead_requests WHERE id = ?", (lead_id,)
        ) as cur:
            row = await cur.fetchone()
        assert row[0] == "pending_verification"

    async def test_logged_in_same_email_skips_verification(self, auth_client, db, test_user):
        """Logged-in user with matching email gets status='new' and 'matched' page."""
        await auth_client.get("/en/leads/quote")
        async with auth_client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await auth_client.post(
            "/en/leads/quote",
            form={
                **self.QUOTE_FORM,
                "contact_email": "test@example.com",  # matches test_user
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200
        html = (await resp.data).decode()
        assert "matched" in html.lower()

        async with db.execute(
            "SELECT status FROM lead_requests WHERE contact_email = 'test@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row[0] == "new"

    async def test_logged_in_different_email_needs_verification(self, auth_client, db, test_user):
        """Logged-in user with different email still needs verification."""
        await auth_client.get("/en/leads/quote")
        async with auth_client.session_transaction() as sess:
            csrf = sess.get("csrf_token", "")

        resp = await auth_client.post(
            "/en/leads/quote",
            form={
                **self.QUOTE_FORM,
                "contact_email": "other@example.com",  # different from test_user
                "csrf_token": csrf,
            },
        )
        assert resp.status_code == 200
        html = (await resp.data).decode()
        assert "check your email" in html.lower()

        async with db.execute(
            "SELECT status FROM lead_requests WHERE contact_email = 'other@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row[0] == "pending_verification"

    async def test_verify_link_activates_lead(self, client, db):
        """GET /leads/verify with valid token sets status='new' and verified_at."""
        lead_id, auth_token, lead_token = await self._submit_guest_quote(client, db)

        resp = await client.get(f"/en/leads/verify?token={auth_token}&lead={lead_token}")
        assert resp.status_code == 200

        async with db.execute(
            "SELECT status, verified_at FROM lead_requests WHERE id = ?", (lead_id,)
        ) as cur:
            row = await cur.fetchone()
        assert row[0] == "new"
        assert row[1] is not None  # verified_at timestamp set

    async def test_verify_sets_session(self, client, db):
        """Verification link logs the user in (sets session user_id)."""
        _lead_id, auth_token, lead_token = await self._submit_guest_quote(client, db)

        await client.get(f"/en/leads/verify?token={auth_token}&lead={lead_token}")

        async with client.session_transaction() as sess:
            assert "user_id" in sess

    async def test_verify_expired_token(self, client, db):
        """Expired/used token redirects with error."""
        _lead_id, auth_token, lead_token = await self._submit_guest_quote(client, db)

        # Expire the token
        await db.execute("UPDATE auth_tokens SET expires_at = '2000-01-01T00:00:00'")
        await db.commit()

        resp = await client.get(
            f"/en/leads/verify?token={auth_token}&lead={lead_token}",
            follow_redirects=False,
        )
        assert resp.status_code == 302

    async def test_verify_already_verified_lead(self, client, db):
        """Attempting to verify an already-activated lead shows error."""
        lead_id, auth_token, lead_token = await self._submit_guest_quote(client, db)

        # Manually activate the lead
        await db.execute(
            "UPDATE lead_requests SET status = 'new' WHERE id = ?", (lead_id,)
        )
        await db.commit()

        resp = await client.get(
            f"/en/leads/verify?token={auth_token}&lead={lead_token}",
            follow_redirects=False,
        )
        assert resp.status_code == 302

    async def test_verify_missing_params(self, client, db):
        """Missing token or lead params redirects."""
        resp = await client.get("/en/leads/verify", follow_redirects=False)
        assert resp.status_code == 302

        resp = await client.get("/en/leads/verify?token=abc", follow_redirects=False)
        assert resp.status_code == 302

    async def test_guest_quote_creates_user(self, client, db):
        """Guest quote submission creates a user row for the contact email."""
        await self._submit_guest_quote(client, db, email="newuser@example.com")

        async with db.execute(
            "SELECT id FROM users WHERE email = 'newuser@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row is not None


# ════════════════════════════════════════════════════════════
# Migration / schema
# ════════════════════════════════════════════════════════════

class TestSchema:
    async def test_schema_has_new_columns(self, db):
        """Fresh DB from schema.sql has all expanded lead_requests columns."""
        async with db.execute("PRAGMA table_info(lead_requests)") as cur:
            cols = {r[1] for r in await cur.fetchall()}
        for expected in (
            "facility_type", "glass_type", "lighting_type",
            "build_context", "country", "timeline",
            "location_status", "financing_status",
            "heat_score", "contact_name", "contact_email",
            "contact_phone", "contact_company",
            "wants_financing_help", "decision_process",
            "previous_supplier_contact", "services_needed",
            "additional_info", "stakeholder_type", "verified_at",
        ):
            assert expected in cols, f"Missing column: {expected}"

    async def test_user_id_nullable(self, db):
        """lead_requests.user_id should accept NULL for guest leads."""
        await db.execute(
            "INSERT INTO lead_requests (lead_type, contact_email, created_at) VALUES (?, ?, datetime('now'))",
            ("quote", "guest@example.com"),
        )
        await db.commit()
        async with db.execute(
            "SELECT user_id FROM lead_requests WHERE contact_email = 'guest@example.com'"
        ) as cur:
            row = await cur.fetchone()
        assert row[0] is None

