"""games_town: the town kit in OhMyGames builds."""

import base64
import hashlib
import io
import tarfile
from pathlib import Path
from types import SimpleNamespace as N

import pytest

import games_town as gt


def _card(title, pitch="", stage="none", view="third", lane="custom", setting=""):
    return N(title=title, pitch=pitch, genre="", core_loop="", first_version="", mood=[],
             world=N(view=view, setting=setting, look=""), lane=lane, stage=stage)


def _tar(files: dict[str, bytes]) -> bytes:
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w:gz") as tf:
        for name, data in files.items():
            info = tarfile.TarInfo(name)
            info.size = len(data)
            tf.addfile(info, io.BytesIO(data))
    return buf.getvalue()


@pytest.fixture
def assets(tmp_path):
    (tmp_path / "townkit.tar.gz").write_bytes(_tar({"TOWN.md": b"town", "settlement.mjs": b"export {}"}))
    (tmp_path / "townkit-medieval.tar.gz").write_bytes(_tar({"town/kits/medieval/kit.glb": b"glb"}))
    (tmp_path / "townkit-breton.tar.gz").write_bytes(_tar({"town/kits/breton/kit.glb": b"glb"}))
    return tmp_path


def _members(att):
    data = base64.b64decode(att["url"].split(",", 1)[1])
    with tarfile.open(fileobj=io.BytesIO(data)) as tf:
        return {m.name: tf.extractfile(m).read() for m in tf.getmembers() if m.isfile()}


@pytest.mark.parametrize("title,pitch,kind", [
    ("Harvest Hamlet", "Run a medieval village through one winter", "medieval"),
    ("Knights of the Reach", "Knights hold a castle town", "medieval"),
    ("Tavern Tales", "Pour ale in a tavern and keep the blacksmith busy", "medieval"),
    ("Kingdom Builder", "A fantasy kingdom with peasants and a feudal lord", "medieval"),
    ("Abbey Ledger", "Keep the monastery bakery and the cathedral fed", "medieval"),
    ("Breton Coast", "A granite village in Brittany", "breton"),
    ("Morbihan Walk", "Walk the lanes of a Breton port", "breton"),
    ("Fishing Village", "Sail out of a fishing village at dawn", "breton"),
])
def test_settlement_words_name_the_style(title, pitch, kind):
    assert gt.town_kind(_card(title, pitch)) == kind


@pytest.mark.parametrize("title,pitch", [
    ("Star Pirates", "Dogfight in space with aliens and asteroids"),
    ("Downtown Heat", "A crime story in a modern city downtown"),
    ("Rally Racer", "Drift a rally car around a racing circuit"),
    ("Keyboard Football", "Score goals in a soccer league"),
    ("Kingdom Hearts Fan Game", "A kingdom hearts style action game"),
    ("Skin Shop", "Sell a market for skins in a fast shooter"),
    ("Neon Metropolis", "Sci-fi skyscrapers and a neon metropolis"),
    ("Crate Crash", "A puzzle about stacking crates in a warehouse"),
])
def test_modern_space_sport_and_everyday_words_are_not_towns(title, pitch):
    assert gt.town_kind(_card(title, pitch)) is None


def test_space_wins_over_settlement_words():
    assert gt.town_kind(_card("Alien Village", "an alien village in space")) is None


def test_prompt_counts_as_words():
    assert gt.town_kind(_card("Quiet Game", "a walk"), "a medieval hamlet at dusk") == "medieval"


@pytest.mark.parametrize("pitch,tier", [
    ("a hamlet of five houses", "hamlet"),
    ("a village with a well", "village"),
    ("a castle town with walls", "town"),
    ("a walled city with a cathedral", "city"),
    ("a medieval market", "village"),
])
def test_tier_from_words(pitch, tier):
    assert gt.town_tier(_card("T", pitch)) == tier


@pytest.mark.parametrize("pitch,site", [
    ("a village on a river bank", "river"),
    ("a fishing village on the coast", "coast"),
    ("a hilltop village", "hill"),
    ("a village at a crossroads", "crossroads"),
    ("a village on a ridge", "spur"),
    ("a village in a field", "plain"),
    ("a hilltop village by the river", "river"),
])
def test_site_from_words(pitch, site):
    assert gt.town_site(_card("T", pitch)) == site


@pytest.mark.parametrize("stage,view,lane,fits", [
    ("none", "third", "custom", True),
    ("room", "first", "custom", False),
    ("none", "top", "custom", True),
    ("board", "third", "custom", False),
    ("none", "side", "custom", False),
    ("none", "third", "quick", False),
])
def test_fits_follows_the_garage_rule(stage, view, lane, fits):
    assert gt.town_fits(_card("Village", "a medieval village", stage=stage, view=view, lane=lane)) is fits


def test_town_attachments_numbered_from_first_index(assets):
    atts, contract, meta = gt.town_attachments(_card("Village", "a medieval village"), 2, "", assets)
    assert [a["filename"] for a in atts] == ["townkit.tar.gz", "townkit-medieval.tar.gz"]
    assert "att3" in contract and "att4" in contract and "att1" not in contract
    assert meta["kind"] == "medieval" and meta["tier"] == "village" and meta["site"] == "plain"


def test_meta_sha_is_the_code_tarball(assets):
    atts, _, meta = gt.town_attachments(_card("Village", "a breton fishing village"), 0, "", assets)
    assert meta["kind"] == "breton"
    assert meta["sha"] == hashlib.sha256((assets / "townkit.tar.gz").read_bytes()).hexdigest()
    (assets / "townkit.sha").write_text("abc123\n")
    assert gt.town_sha(assets) == "abc123"


def test_attachment_bytes_are_the_bundles(assets):
    atts, _, _ = gt.town_attachments(_card("Village", "a medieval village"), 0, "", assets)
    assert _members(atts[0])["TOWN.md"] == b"town"
    assert "town/kits/medieval/kit.glb" in _members(atts[1])


def test_contract_names_the_files_and_the_laws(assets):
    _, contract, _ = gt.town_attachments(_card("Harbour", "a fishing village on the coast"), 0, "", assets)
    for needle in ("src/lib/town", "public/town", "TOWN.md", "SETTLEMENT.md", "createTown(", "town.group", "town.colliders",
                   "town.heightAt", "tier: 'village'", "site: 'coast'", "style: 'breton'", "main square"):
        assert needle in contract, needle


def test_missing_bundle_means_no_town(tmp_path, assets):
    empty = tmp_path / "empty"
    empty.mkdir()
    assert gt.town_attachments(_card("Village", "a medieval village"), 0, "", empty) is None
    (assets / "townkit-medieval.tar.gz").unlink()
    assert gt.town_attachments(_card("Village", "a medieval village"), 0, "", assets) is None


def test_missing_style_bundle_only_drops_that_style(assets):
    (assets / "townkit-breton.tar.gz").unlink()
    assert gt.town_attachments(_card("Breton", "a Breton village"), 0, "", assets) is None
    assert gt.town_attachments(_card("Village", "a medieval village"), 0, "", assets) is not None


def test_no_settlement_no_town(assets):
    assert gt.town_attachments(_card("Space", "dogfight in space"), 0, "", assets) is None
    assert gt.town_attachments(_card("Village", "a medieval village", stage="board"), 0, "", assets) is None
