"""Tests that assert behaviour, not that something rendered (react.md).

These cover the pure-logic contracts that do not need a database; the
end-to-end guarantees (numbering durability, media authorisation, delete
refusal) are asserted against the running deployment by scripts/gate-p0.sh.
"""

from __future__ import annotations

import csv
from pathlib import Path

import pytest

from app.services.exports import MOVEMENT_COLUMNS, TICKET_COLUMNS
from app.services.media import ALLOWED_MIME, MAX_BYTES, MediaError, store
from app.services.tickets import display_number

SEED_DIR = Path(__file__).resolve().parent.parent / "seed"


class TestDisplayNumber:
    def test_pads_to_six_digits(self):
        assert display_number(1) == "CP-000001"

    def test_does_not_truncate_large_numbers(self):
        assert display_number(1234567) == "CP-1234567"


class TestMediaValidation:
    def test_rejects_unlisted_mime(self):
        with pytest.raises(MediaError) as exc:
            store(b"x", "application/x-msdownload")
        assert exc.value.error_code == "MEDIA_TYPE_REJECTED"

    def test_rejects_empty_upload(self):
        with pytest.raises(MediaError) as exc:
            store(b"", "image/png")
        assert exc.value.error_code == "MEDIA_EMPTY"

    def test_rejects_oversize_upload(self):
        with pytest.raises(MediaError) as exc:
            store(b"x" * (MAX_BYTES + 1), "image/png")
        assert exc.value.error_code == "MEDIA_TOO_LARGE"

    def test_executable_types_are_not_reachable_by_extension(self):
        # The allowlist is the whole defence - assert nothing script-ish is in it.
        assert not any(
            ext in {".php", ".js", ".html", ".svg"} for ext in ALLOWED_MIME.values()
        )


class TestExportContract:
    """The exports must stay recognisable to someone who uses Mike's own report."""

    def test_movement_export_carries_the_source_report_fields(self):
        # Field names taken from notes/.../BP Parts Report.xlsx. If a rename
        # lands, this fails and the CSV stops silently drifting from his sheet.
        required = {
            "Transaction ID",
            "Direction",
            "Part ID",
            "Part Name",
            "Part Types",
            "QR/Bar code",
            "Part Location",
            "Unit Cost",
            "Total Cost",
            "U/M",
            "Part #",
        }
        assert required <= set(MOVEMENT_COLUMNS)

    def test_movement_export_ties_every_row_to_a_ticket(self):
        assert "Work Order ID" in MOVEMENT_COLUMNS

    def test_ticket_export_carries_status_and_audit_fields(self):
        required = {
            "Ticket Number",
            "Status",
            "Priority",
            "Asset",
            "Reported By",
            "Reported At",
            "Closed At",
            "Parent Ticket",
        }
        assert required <= set(TICKET_COLUMNS)


class TestSeedFixtures:
    def test_assets_fixture_matches_the_source_equipment_list(self):
        rows = list(csv.DictReader((SEED_DIR / "assets.csv").open(encoding="utf-8")))
        assert len(rows) == 376
        assert all(row["equipment_no"] and row["description"] for row in rows)

    def test_asset_equipment_numbers_are_unique(self):
        rows = list(csv.DictReader((SEED_DIR / "assets.csv").open(encoding="utf-8")))
        numbers = [row["equipment_no"] for row in rows]
        assert len(numbers) == len(set(numbers))

    def test_parts_fixture_has_no_residual_mojibake(self):
        # The source export decoded UTF-8 as cp1252; extract_seed.py repairs it.
        rows = list(csv.DictReader((SEED_DIR / "parts.csv").open(encoding="utf-8")))
        assert rows
        offenders = [r["part_number"] for r in rows if "â€" in r["part_number"]]
        assert offenders == []

    def test_part_numbers_fit_the_column(self):
        rows = list(csv.DictReader((SEED_DIR / "parts.csv").open(encoding="utf-8")))
        assert max(len(r["part_number"]) for r in rows) <= 128


class TestSecretPlaceholderGuard:
    """The server stages secrets as REPLACE_WITH_* placeholders for a human to
    fill. An unfilled placeholder must read as absent, or the feature reports
    itself configured and then fails at first use."""

    def test_placeholder_is_not_a_key(self, monkeypatch):
        from app.config import Settings

        settings = Settings(gemini_api_key="REPLACE_WITH_GEMINI_API_KEY")
        assert settings.has_gemini_key is False

    def test_empty_is_not_a_key(self):
        from app.config import Settings

        assert Settings(gemini_api_key="").has_gemini_key is False
        assert Settings(gemini_api_key="   ").has_gemini_key is False

    def test_a_real_looking_key_is_a_key(self):
        from app.config import Settings

        assert Settings(gemini_api_key="AIzaSyExampleNotARealKey").has_gemini_key


class TestAudioMimeNormalisation:
    def test_mediarecorder_codec_parameter_is_stripped(self):
        from app.services.ai.transcribe import normalise_audio_mime

        assert normalise_audio_mime("audio/webm;codecs=opus") == "audio/webm"
        assert normalise_audio_mime("AUDIO/MP4") == "audio/mp4"

    def test_ios_and_android_containers_are_both_supported(self):
        from app.services.ai.transcribe import SUPPORTED_AUDIO_MIME

        assert "audio/mp4" in SUPPORTED_AUDIO_MIME  # iOS Safari
        assert "audio/webm" in SUPPORTED_AUDIO_MIME  # Chrome / Android
