# Exercise real block geometry over a finite corpus, targeted cases, and deliberate defects.
from __future__ import annotations

import copy
import json
import subprocess
import sys
from pathlib import Path

import pytest
from generate import Build, generate, blockstates_equivalent
from blocks import load_schematic
from mcio.nbt.nbt import load as load_nbt
from lodge.plan import DEFAULT_SEED, DOMAINS, resolve
from lodge.geometry import DIRECTIONS, blockstate, name_of
from lodge.validation import validate_scene, envelope_leaks, geometry_signature, parse, reachable
from cases import TARGETS

CORPUS = list(range(100)) + [-(2**63), 2**63 - 1, DEFAULT_SEED, 20260923]


@pytest.mark.parametrize('seed', CORPUS)
def test_seed_corpus(seed):
    build = generate(seed)
    scene = build.scene
    for room in scene.rooms:
        x0, y0, z0, x1, y1, z1 = room['bounds']
        contents = {name_of(state) for (x, y, z), state in scene.blocks.items() if x0 <= x <= x1 and y0 <= y <= y1 and z0 <= z <= z1}
        if 'kitchen' in room['purpose']:
            assert 'smoker' in contents and 'crafting_table' in contents, (seed, room)
        if 'bed' in room['purpose'] or 'suite' in room['purpose']:
            assert any(name.endswith('_bed') for name in contents), (seed, room)
    visited = reachable(scene)
    for foot in scene.beds:
        x, y, z = foot
        _, props = parse(scene.blocks[foot])
        dx, dz = DIRECTIONS[props['facing']]
        assert any((xx + a, y, zz + b) in visited for xx, zz in ((x, z), (x + dx, z + dz)) for a, b in DIRECTIONS.values()), (seed, foot)


@pytest.mark.parametrize('name,parameters', TARGETS, ids=[n for n, _ in TARGETS])
def test_targeted_geometry(name, parameters):
    build = generate(907, parameters)
    assert build.validation['envelope_checked']
    assert all(any(r['volume'] == v.name and r['floor'] == f for r in build.scene.rooms) for v in build.plan.volumes for f in v.levels)


@pytest.mark.parametrize('seed', [0, 8, 31, DEFAULT_SEED, -(2**63), 2**63 - 1])
def test_determinism_and_semantic_roundtrip(seed, artifact_dir):
    a = generate(seed, output=artifact_dir / 'a.litematic')
    b = generate(seed, output=artifact_dir / 'b.litematic')
    assert a.placed == b.placed
    assert (artifact_dir / 'a.litematic').read_bytes() == (artifact_dir / 'b.litematic').read_bytes()
    assert a.plan.record() == b.plan.record()
    root = load_nbt(artifact_dir / 'a.litematic')
    assert len(root['Regions']) == 1
    assert int(root['Metadata']['TimeCreated']) == int(root['Metadata']['TimeModified']) == 0
    loaded = load_schematic(artifact_dir / 'a.litematic')
    assert loaded.size_yzx == (a.size_y, a.size_z, a.size_x)
    ids = loaded.read_flat(0, loaded.volume).reshape(loaded.size_yzx)
    ox, oy, oz = a.plan.origin
    scene = copy.copy(a.scene)
    scene.blocks = {p: loaded.palette[int(ids[p[1] + oy, p[2] + oz, p[0] + ox])] for p in a.scene.blocks}
    assert geometry_signature(scene) == geometry_signature(a.scene)
    validate_scene(scene)
    record = json.loads((artifact_dir / 'a.json').read_text())
    replay = generate(record['seed'], record['overrides'])
    assert replay.placed == a.placed


def test_semantic_property_comparison_and_overwrite(artifact_dir):
    left = 'minecraft:oak_stairs[facing=north,half=bottom,shape=straight]'
    reordered = 'minecraft:oak_stairs[shape=straight,half=bottom,facing=north]'
    assert blockstates_equivalent(left, reordered)
    assert not blockstates_equivalent(left, left.replace('north', 'south'))
    assert not blockstates_equivalent(left, 'minecraft:oak_stairs[facing=north,half=bottom]')
    assert not blockstates_equivalent('minecraft:stone', 'minecraft:cobblestone')
    build = Build(31).build()
    build.put(0, 0, 0, 'minecraft:dirt')
    build.put(0, 0, 0, 'minecraft:oak_stairs', facing='north', half='bottom')
    build.export(artifact_dir / 'overwrite.litematic')
    build.verify(artifact_dir / 'overwrite.litematic')
    with pytest.raises(AssertionError, match='out of bounds'):
        build.put(-1, 0, 0, 'minecraft:stone')


@pytest.mark.parametrize('params', [
    {
        'width': 16
    },
    {
        'floors': True
    },
    {
        'unknown': 1
    },
    {
        'floors': 1,
        'wing_floors': 2
    },
    {
        'floors': 1,
        'balcony': True
    },
    {
        'topology': 'courtyard',
        'porch': 'wrap'
    },
])
def test_invalid_explicit_parameters(params):
    with pytest.raises(ValueError):
        resolve(0, params)


@pytest.mark.parametrize('seed', [True, -(2**63) - 1, 2**63, '7'])
def test_seed_domain(seed):
    with pytest.raises(ValueError, match='signed 64-bit'):
        resolve(seed)


def test_resource_limits_are_enforced():
    for limits in ({'max_axis': False}, {'max_height': -1}, {'tree_height': [4, 50]}, {'tree_height': [25, 20]}):
        with pytest.raises(ValueError):
            resolve(0, limits=limits)
    with pytest.raises(ValueError, match='resource limits'):
        resolve(0, limits={'max_axis': 20})
    with pytest.raises(ValueError, match='floor_height'):
        resolve(0, limits={'floor_height': 5})
    with pytest.raises(ValueError, match='unknown resource limit'):
        resolve(0, limits={'hidden': 100})
    with pytest.raises(ValueError, match='not a Java'):
        blockstate('unobtainium')
    with pytest.raises(ValueError, match='invalid'):
        blockstate('oak_stairs', (('facing', 'up'),))


def test_observed_structural_diversity():
    signatures = set()
    coverage = {k: set() for k in ('topology', 'floors', 'ridge', 'profile', 'tower', 'conservatory', 'porch', 'balcony', 'dormers', 'terrain', 'entrance', 'layout', 'stair_side')}
    for seed in range(40):
        b = Build(seed).build()
        signatures.add(geometry_signature(b.scene))
        for key in coverage:
            coverage[key].add(b.plan.parameters[key])
    assert len(signatures) == 40
    for key, observed in coverage.items():
        assert observed == set(DOMAINS[key]), (key, observed)
    # Change a genuine room-division parameter under the same massing.
    params = dict(TARGETS[0][1], floors=2)
    a = Build(31, dict(params, layout='open')).build()
    b = Build(31, dict(params, layout='suites')).build()
    assert a.plan.signature() == b.plan.signature()
    assert geometry_signature(a.scene) != geometry_signature(b.scene)


@pytest.mark.parametrize('defect', ['door', 'bed', 'stair_headroom', 'ladder_support', 'window', 'entry'])
def test_actual_geometry_defects_are_rejected(defect):
    scene = Build(DEFAULT_SEED).build().scene
    if defect == 'door':
        x, y, z = scene.doors[0]
        scene.p(x, y + 1, z, 'air')
    elif defect == 'bed':
        x, y, z = scene.beds[0]
        scene.p(x, y, z, 'air')
    elif defect == 'stair_headroom':
        x, y, z, _ = scene.stairs[-1]
        scene.p(x, y + 2, z, 'stone')
    elif defect == 'ladder_support':
        x, y, z = scene.ladders[0]
        _, props = parse(scene.blocks[x, y, z])
        dx, dz = DIRECTIONS[props['facing']]
        scene.p(x - dx, y, z - dz, 'air')
    elif defect == 'window':
        scene.p(*scene.windows[0]['cells'][0], 'stone')
    else:
        x, y, z = scene.plan.entrance
        scene.box((x, y, z, x, y + 2, z), 'stone')
    with pytest.raises(AssertionError):
        validate_scene(scene)


def test_envelope_defect_is_rejected():
    scene = Build(907, TARGETS[0][1]).build().scene
    v = scene.plan.volumes[0]
    x, z = (v.x0 + v.x1) // 2, (v.z0 + v.z1) // 2
    h = v.roof(x, z)
    scene.clear((x, h - 2, z, x, h + 3, z))
    assert envelope_leaks(scene)


def test_cli_clean_directory(artifact_dir):
    script = Path(__file__).resolve().parents[1] / 'generate.py'
    subprocess.run([sys.executable, str(script), '--seed', '31'], cwd=artifact_dir, check=True, capture_output=True)
    assert (artifact_dir / 'output.litematic').is_file()
    assert json.loads((artifact_dir / 'output.json').read_text())['seed'] == 31
