"""Applications: a form's schedule (app/crud/form_schedule.py), officers'
review with remarks, history and corrections (app/crud/application_review.py),
the public status page and public file uploads (app/routers/public_forms.py),
uniqueness per program and across line-item rows (app/crud/form_stored_rules.py),
duplicating a form, and CSV exports. Delivery is mocked."""

import re
import uuid
from datetime import datetime, timedelta, timezone
from unittest.mock import patch

import pytest

from app.core import jobs, throttle
from app.models.reporting import Connector
from tests.conftest import AsyncSessionLocal


@pytest.fixture(autouse=True)
def _fresh_rate_limits():
    throttle.reset()
    yield
    throttle.reset()


async def _form(client, headers, fields: list[dict], **settings) -> dict:
    resp = await client.post("/forms", headers=headers, json={"name": f"Pytest App {uuid.uuid4().hex[:6]}", "settings": settings, "field_definitions": fields})
    assert resp.status_code == 201, resp.text
    published = await client.post(f"/forms/{resp.json()['form_id']}/publish", headers=headers)
    assert published.status_code == 200, published.text
    return published.json()


async def _submit(client, headers, form, values: dict):
    return await client.post("/form-submissions", headers=headers, json={"form_id": form["form_id"], "form_version": form["version"], "field_values": values})


def _iso(delta: timedelta) -> str:
    return (datetime.now(timezone.utc) + delta).isoformat()


SIMPLE = [{"name": "name", "type": "text", "label": "Name", "required": True}]


async def test_schedule_opens_pauses_and_closes(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    form = await _form(client, auth_headers, SIMPLE, public_access=True, schedule={"opens_at": _iso(timedelta(days=2))})
    base = f"/public/forms/{tid}/{form['form_id']}"

    got = (await client.get(base)).json()
    assert got["availability"] == "scheduled" and got["availability_message"].startswith("This form opens on")
    refused = await client.post(f"{base}/submissions", json={"field_values": {"name": "Asha"}})
    assert refused.status_code == 409 and "opens on" in refused.json()["detail"]
    signed_in = await _submit(client, auth_headers, form, {"name": "Asha"})
    assert signed_in.status_code == 409

    # A new version with the schedule changed: paused with a custom line.
    fid = form["form_id"]
    await client.post(f"/forms/{fid}/new-version", headers=auth_headers)
    draft = (await client.get(f"/forms/{fid}", headers=auth_headers)).json()
    paused = await client.patch(f"/forms/{fid}", headers=auth_headers, json={"settings": {**draft["settings"], "schedule": {"paused": True, "message": "Back on Monday."}}})
    assert paused.status_code == 200, paused.text
    assert (await client.post(f"/forms/{fid}/publish", headers=auth_headers)).status_code == 200
    got = (await client.get(base)).json()
    assert (got["availability"], got["availability_message"]) == ("paused", "Back on Monday.")

    # Closed: the dates are checked, and the closing date must come after the opening.
    await client.post(f"/forms/{fid}/new-version", headers=auth_headers)
    bad = await client.patch(f"/forms/{fid}", headers=auth_headers, json={"settings": {**draft["settings"], "schedule": {"opens_at": _iso(timedelta(days=2)), "closes_at": _iso(timedelta(days=1))}}})
    assert bad.status_code == 422
    await client.patch(f"/forms/{fid}", headers=auth_headers, json={"settings": {**draft["settings"], "schedule": {"opens_at": _iso(-timedelta(days=5)), "closes_at": _iso(-timedelta(days=1))}}})
    await client.post(f"/forms/{fid}/publish", headers=auth_headers)
    assert (await client.get(base)).json()["availability"] == "closed"
    assert (await client.post(f"{base}/submissions", json={"field_values": {"name": "Asha"}})).status_code == 409

    # Open again — straight away, on the live version, without a new version.
    reopened = await client.put(f"/forms/{fid}/schedule", headers=auth_headers, json={"closes_at": _iso(timedelta(days=30))})
    assert reopened.status_code == 200, reopened.text
    assert reopened.json()["status"] == "published"
    assert (await client.get(base)).json()["availability"] == "open"
    assert (await client.post(f"{base}/submissions", json={"field_values": {"name": "Asha"}})).status_code == 201
    # Archived: closed for good, its submissions kept.
    await client.put(f"/forms/{fid}/schedule", headers=auth_headers, json={"archived": True})
    assert (await client.get(base)).json()["availability"] == "archived"
    assert len((await client.get(f"/forms/{fid}/submissions", headers=auth_headers)).json()) == 1


GROUP = [
    {"name": "ref", "type": "autonumber", "label": "Application number", "autonumber_format": "KS/{SEQ:4}"},
    {"name": "program", "type": "dropdown", "label": "Program", "options": ["Campus 2026", "Campus 2027"], "required": True},
    {"name": "group_name", "type": "text", "label": "Group name", "required": True},
    {"name": "email", "type": "email", "label": "Email"},
    {"name": "status", "type": "text", "label": "Status"},
    {
        "name": "members",
        "type": "subform",
        "label": "Members",
        "min_rows": 2,
        "max_rows": 4,
        "subform_fields": [
            {"name": "member", "type": "text", "label": "Name", "required": True},
            {"name": "reg_no", "type": "text", "label": "Register number", "required": True, "validation_rules": [{"type": "unique", "scope_fields": ["program"]}]},
        ],
    },
]
REVIEW = {"enabled": True, "status_field": "status", "notify_field": "email", "correction_days": 7}


def _group(program: str, name: str, *regs: str) -> dict:
    return {"program": program, "group_name": name, "email": "lead@example.com", "members": [{"member": f"M{r}", "reg_no": r} for r in regs]}


async def test_student_in_one_group_per_program(client, auth_headers):
    form = await _form(client, auth_headers, GROUP)
    first = await _submit(client, auth_headers, form, _group("Campus 2026", "A", "R1", "R2"))
    assert first.status_code == 201, first.text

    # The same student twice in one group.
    twice = await _submit(client, auth_headers, form, _group("Campus 2026", "B", "R3", " r3 "))
    assert twice.status_code == 422
    assert "Rows 1 and 2 have the same Register number" in twice.text
    # Already in another group of the same program (any case / spacing).
    taken = await _submit(client, auth_headers, form, _group("Campus 2026", "C", "R4", "r2"))
    assert taken.status_code == 422 and "Row 2: Register number is already used in another submission." in taken.text
    # Another program is fine.
    other = await _submit(client, auth_headers, form, _group("Campus 2027", "D", "R1", "R2"))
    assert other.status_code == 201, other.text
    # Editing a submission doesn't clash with itself.
    edited = await client.patch(f"/form-submissions/{first.json()['id']}", headers=auth_headers, json={"field_values": {**_group("Campus 2026", "A2", "R1", "R2")}})
    assert edited.status_code == 200, edited.text


async def test_unique_scoped_to_a_field(client, auth_headers):
    fields = [
        {"name": "program", "type": "text", "label": "Program"},
        {"name": "aadhaar", "type": "text", "label": "ID", "validation_rules": [{"type": "unique", "scope_fields": ["program"]}]},
    ]
    form = await _form(client, auth_headers, fields)
    assert (await _submit(client, auth_headers, form, {"program": "P1", "aadhaar": "X1"})).status_code == 201
    assert (await _submit(client, auth_headers, form, {"program": "P1", "aadhaar": "x1"})).status_code == 422
    assert (await _submit(client, auth_headers, form, {"program": "P2", "aadhaar": "X1"})).status_code == 201
    # scope_fields belongs to "unique" only.
    bad = await client.post("/forms", headers=auth_headers, json={"name": "x", "field_definitions": [{"name": "a", "type": "text", "label": "A", "validation_rules": [{"type": "email", "scope_fields": ["b"]}]}]})
    assert bad.status_code == 422


async def test_review_return_correct_and_approve(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    async with AsyncSessionLocal() as db, db.begin():
        db.add(Connector(tenant_id=tid, name="Pytest mail", type="email_smtp", configuration={"host": "smtp.example.com", "from": "noreply@example.com"}, status="enabled"))
    form = await _form(client, auth_headers, GROUP, review=REVIEW)
    created = await _submit(client, auth_headers, form, _group("Campus 2026", "Stars", "S1", "S2"))
    assert created.status_code == 201, created.text
    sub = created.json()
    assert sub["review_status"] == "submitted" and sub["field_values"]["status"] == "Submitted"
    sid = sub["id"]

    # Returning needs a remark; it names the fields that may change.
    no_note = await client.post(f"/form-submissions/{sid}/review", headers=auth_headers, json={"status": "returned"})
    assert no_note.status_code == 422
    returned = await client.post(
        f"/form-submissions/{sid}/review", headers=auth_headers, json={"status": "returned", "note": "Fix the group name.", "correction_fields": ["group_name"]}
    )
    assert returned.status_code == 200, returned.text
    body = returned.json()
    assert body["review_status"] == "returned" and body["correction_fields"] == ["group_name"] and body["field_values"]["status"] == "Returned for correction"
    assert body["correction_until"] is not None

    # Filtered lists.
    listed = await client.get(f"/forms/{form['form_id']}/submissions", headers=auth_headers, params={"review_status": "returned"})
    assert [r["id"] for r in listed.json()] == [sid]
    count = await client.get(f"/forms/{form['form_id']}/submissions/count", headers=auth_headers, params={"review_status": "approved"})
    assert count.json()["total"] == 0

    # The applicant is told, with the remark.
    sent: list[tuple] = []
    with patch("app.crud.tasks._send_email_sync", side_effect=lambda cfg, to, subject, body, *a: sent.append((to, subject, body))):
        await jobs.run_due(["application.status_notice"], tenant_id=tid)
    assert sent and sent[-1][0] == "lead@example.com" and "Returned for correction" in sent[-1][2] and "Fix the group name." in sent[-1][2]

    # The submitter corrects it: only the named field changes.
    fixed = await client.post(
        f"/form-submissions/{sid}/resubmit", headers=auth_headers, json={"field_values": {"group_name": "Shining Stars", "program": "Campus 2027", "status": "Approved"}}
    )
    assert fixed.status_code == 200, fixed.text
    values = fixed.json()["field_values"]
    assert values["group_name"] == "Shining Stars" and values["program"] == "Campus 2026" and values["status"] == "Resubmitted"
    assert fixed.json()["review_status"] == "resubmitted"
    # Not twice.
    assert (await client.post(f"/form-submissions/{sid}/resubmit", headers=auth_headers, json={"field_values": {}})).status_code == 409

    approved = await client.post(f"/form-submissions/{sid}/review", headers=auth_headers, json={"status": "approved", "note": "Welcome!"})
    assert approved.json()["review_status"] == "approved"
    history = (await client.get(f"/form-submissions/{sid}/reviews", headers=auth_headers)).json()
    assert [h["to_status"] for h in history] == ["submitted", "returned", "resubmitted", "approved"]
    assert history[1]["note"] == "Fix the group name." and history[1]["actor_name"]


async def test_public_status_page_and_correction(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    async with AsyncSessionLocal() as db, db.begin():
        db.add(Connector(tenant_id=tid, name="Pytest mail", type="email_smtp", configuration={"host": "smtp.example.com", "from": "noreply@example.com"}, status="enabled"))
    fields = [
        {"name": "ref", "type": "autonumber", "label": "Application number", "autonumber_format": "KSC/{SEQ:4}"},
        {"name": "name", "type": "text", "label": "Name", "required": True},
        {"name": "college", "type": "text", "label": "College", "required": True},
        {"name": "email", "type": "email", "label": "Email", "require_verification": True},
    ]
    review = {"enabled": True, "status_lookup": True, "lookup_reference_field": "ref", "lookup_verify_field": "email"}
    form = await _form(client, auth_headers, fields, public_access=True, review=review)
    base = f"/public/forms/{tid}/{form['form_id']}"

    # Submitted by signed-in staff on the applicant's behalf, for brevity.
    created = await client.post(
        "/form-submissions",
        headers=auth_headers,
        json={"form_id": form["form_id"], "form_version": form["version"], "field_values": {"name": "Asha", "college": "MG", "email": "asha@example.com"}},
    )
    # The email needs a code even here — so add it without one, as staff editing.
    if created.status_code == 422:
        created = await _submit(client, auth_headers, form, {"name": "Asha", "college": "MG"})
        assert created.status_code == 201, created.text
        sid = created.json()["id"]
        edited = await client.patch(f"/form-submissions/{sid}", headers=auth_headers, json={"field_values": {**created.json()["field_values"], "email": "asha@example.com"}})
        assert edited.status_code == 200, edited.text
    sid = created.json()["id"]
    reference = created.json()["field_values"]["ref"]

    assert (await client.post(f"{base}/status/send", json={"reference": "KSC/9999"})).status_code == 404
    sent: list[tuple] = []
    with patch("app.crud.form_verification._send_email_sync", side_effect=lambda cfg, to, subject, body: sent.append((to, body))):
        started = await client.post(f"{base}/status/send", json={"reference": reference.lower()})
    assert started.status_code == 200, started.text
    assert started.json()["sent_to"] == "a•••@example.com" and sent[0][0] == "asha@example.com"
    code = re.search(r"\b(\d{6})\b", sent[0][1]).group(1)
    token = (await client.post(f"{base}/status/confirm", json={"reference": reference, "verification_id": started.json()["verification_id"], "code": code})).json()["token"]

    view = (await client.post(f"{base}/status/view", json={"token": token})).json()
    assert view["status"] == "submitted" and view["reference"] == reference and not view["can_correct"] and view["field_values"] == {}
    assert (await client.post(f"{base}/status/view", json={"token": "nope"})).status_code == 401

    await client.post(f"/form-submissions/{sid}/review", headers=auth_headers, json={"status": "returned", "note": "College name is incomplete.", "correction_fields": ["college"]})
    view = (await client.post(f"{base}/status/view", json={"token": token})).json()
    assert view["can_correct"] and view["note"] == "College name is incomplete." and view["correction_fields"] == ["college"]
    assert view["field_values"]["college"] == "MG"

    fixed = await client.post(f"{base}/status/resubmit", json={"token": token, "field_values": {"college": "Maharaja's College", "name": "Someone else"}})
    assert fixed.status_code == 200, fixed.text
    assert fixed.json()["status"] == "resubmitted" and not fixed.json()["can_correct"]
    stored = (await client.get(f"/form-submissions/{sid}", headers=auth_headers)).json()["field_values"]
    assert stored["college"] == "Maharaja's College" and stored["name"] == "Asha"
    assert [h["status"] for h in fixed.json()["history"]] == ["submitted", "returned", "resubmitted"]

    # A form without a status page has none.
    plain = await _form(client, auth_headers, SIMPLE, public_access=True)
    assert (await client.post(f"/public/forms/{tid}/{plain['form_id']}/status/send", json={"reference": "x"})).status_code == 404


async def test_public_file_upload(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    fields = [
        {"name": "name", "type": "text", "label": "Name"},
        {"name": "report", "type": "file_upload", "label": "Project report", "accept": ".pdf"},
        {"name": "photos", "type": "multi_attachment", "label": "Photos", "accept": "image/*", "max_files": 2, "max_file_size_mb": 1},
    ]
    form = await _form(client, auth_headers, fields, public_access=True)
    base = f"/public/forms/{tid}/{form['form_id']}"

    wrong_type = await client.post(f"{base}/files", data={"field": "report"}, files={"file": ("notes.txt", b"hello", "text/plain")})
    assert wrong_type.status_code == 422
    too_big = await client.post(f"{base}/files", data={"field": "photos"}, files={"file": ("big.png", b"x" * (1024 * 1024 + 10), "image/png")})
    assert too_big.status_code == 413
    no_field = await client.post(f"{base}/files", data={"field": "name"}, files={"file": ("a.pdf", b"%PDF", "application/pdf")})
    assert no_field.status_code == 422
    pdf = await client.post(f"{base}/files", data={"field": "report"}, files={"file": ("report.pdf", b"%PDF-1.4 test", "application/pdf")})
    assert pdf.status_code == 201, pdf.text
    photo = await client.post(f"{base}/files", data={"field": "photos"}, files={"file": ("a.png", b"\x89PNG", "image/png")})
    meta = await client.get(f"{base}/files/{pdf.json()['id']}")
    assert meta.json()["filename"] == "report.pdf"

    # A workspace file can't be attached by a visitor.
    staff = await client.post("/uploads", headers=auth_headers, files={"file": ("internal.pdf", b"%PDF secret", "application/pdf")})
    sneaky = await client.post(f"{base}/submissions", json={"field_values": {"name": "X", "report": staff.json()["id"]}})
    assert sneaky.status_code == 422
    assert (await client.get(f"{base}/files/{staff.json()['id']}")).status_code == 404

    ok = await client.post(f"{base}/submissions", json={"field_values": {"name": "X", "report": pdf.json()["id"], "photos": [photo.json()["id"]]}})
    assert ok.status_code == 201, ok.text
    # Staff can open it.
    download = await client.get(f"/uploads/{pdf.json()['id']}/download", headers=auth_headers)
    assert download.status_code == 200 and download.content == b"%PDF-1.4 test"


async def test_duplicate_form_and_csv_exports(client, auth_headers):
    form = await _form(client, auth_headers, SIMPLE, schedule={"closes_at": _iso(timedelta(days=3)), "paused": True}, review=REVIEW)
    dup = await client.post(f"/forms/{form['form_id']}/duplicate", headers=auth_headers, json={})
    assert dup.status_code == 201, dup.text
    copy = dup.json()
    assert copy["form_id"] != form["form_id"] and copy["name"] == f"Copy of {form['name']}" and copy["status"] == "draft"
    assert copy["field_definitions"][0]["name"] == "name"
    assert copy["settings"]["schedule"]["closes_at"] is None and copy["settings"]["schedule"]["paused"] is False
    assert copy["settings"]["review"]["enabled"] is True
    named = await client.post(f"/forms/{form['form_id']}/duplicate", headers=auth_headers, json={"name": "Campus 2027"})
    assert named.json()["name"] == "Campus 2027"

    target = await _form(client, auth_headers, [{"name": "name", "type": "text", "label": "Name"}, {"name": "district", "type": "text", "label": "District"}])
    for name, district in (("=cmd", "Ernakulam"), ("കുടുംബശ്രീ", "Thrissur")):
        assert (await _submit(client, auth_headers, target, {"name": name, "district": district})).status_code == 201
    csv = await client.post(
        "/datasets/export.csv", headers=auth_headers, json={"source_type": "form", "source_id": target["form_id"], "columns": ["name", "district"], "sort_field": "district"}
    )
    assert csv.status_code == 200, csv.text
    text = csv.content.decode("utf-8-sig").splitlines()
    assert text[0] == "name,district" and "'=cmd,Ernakulam" in text and "കുടുംബശ്രീ,Thrissur" in text
