# Actual-geometry damage tests, independent environment sampling and regeneration.
from dataclasses import replace, dataclass
from pathlib import Path
import pytest
from hearth import Scene, Box, Frame, Binding, ContractError
from hearth.kernel import validate, Capability, Contract, Plan, Port, Grant, Child, Rule
from hearth.kernel.scene import Cell
from hearth.blocks import state_of, name_of, blockstates_equivalent, validate_state
from hearth.environment import Terrain, constant, gradient, wave, radial
from hearth.components.building import Building, Blueprint, RoomSpec
from hearth.components.primitives import Pool, Volume
from hearth.programs import PROGRAMS, building_scene, settlement_scene
from hearth.assessment import adaptation_expression, principal_signature
from hearth.persistence import export_scene, load_scene, semantic_digest, companion_path, _mcio


def damage(scene, p, material=None):
    scene = scene._fork()
    old = scene.blocks.get(p)
    if old:
        scene.reverse[old.owner].discard(p)
        scene.blocks.pop(p)
    if material:
        owner = old.owner if old else next(n.path for n in scene.nodes.values() if n.type.startswith('room.'))
        operation = old.operation if old else next(iter(scene.operations))
        scene.blocks[p] = Cell(material, owner, operation)
        scene.reverse[owner].add(p)
    return scene


@pytest.fixture
def house():
    return building_scene(Blueprint((RoomSpec(0, 0, 0, 'living'), RoomSpec(0, 0, 1, 'bedroom'))), 7, gradient(.04, 0, 4), 6)


def test_removed_support_blocked_stair_exit_and_paired_door(house):
    foundation = next(n for n in house.nodes.values() if n.type == 'adapter.foundation')
    p = foundation.contract.rules[0].cells[0]
    assert 'support' in {d.rule for d in validate(damage(house, p), True)}
    stair = next(n for n in house.nodes.values() if n.type == 'circulation.stair')
    p = next(r for r in stair.contract.rules if r.kind == 'clear').cells[4]
    assert 'clearance' in {d.rule for d in validate(damage(house, p, state_of('stone')), True)}
    door = next(p for p, c in house.blocks.items() if 'spruce_door' in c.state and 'half=lower' in c.state)
    issues = validate(damage(house, door, state_of('stone')), True)
    assert {'door-pair', 'reachability'} <= {d.rule for d in issues}
    assert all(d.component.startswith('/') and d.conditions for d in issues)


def test_lights_furniture_seal_and_inventory_are_actual(house):
    room = '/building/room-0-0-0'
    s = house
    for p in house.cells(room, True):
        if name_of(house.blocks[p].state) == 'lantern':
            s = damage(s, p)
    assert 'lighting' in {d.rule for d in validate(s, True)}
    glass = next(p for p in house.cells(room, True) if name_of(house.blocks[p].state) == 'glass')
    assert 'enclosure' in {d.rule for d in validate(damage(house, glass), True)}
    bed = next(p for p, c in house.blocks.items() if name_of(c.state) == 'red_bed')
    assert 'bed-pair' in {d.rule for d in validate(damage(house, bed), True)}
    barrel = next(p for p, c in house.blocks.items() if c.nbt)
    s = house._fork()
    nbt = dict(s.blocks[barrel].nbt)
    nbt['Items'] = [{'Slot': 0, 'id': 'minecraft:bread', 'count': 100}]
    s.blocks[barrel] = replace(s.blocks[barrel], nbt=nbt)
    assert 'inventory' in {d.rule for d in validate(s, True)}


def test_pool_boundary_damage_and_unsupported_substrate():
    s = Scene(domain=Box((-3, -3, -3), (14, 20, 14)))
    s.place('land', Terrain(s.domain, constant(0)))
    s.place('pool', Pool(), bindings=(Binding('/land', 'construction', 'excavate'),))
    assert 'pool-containment' in {d.rule for d in validate(damage(s, (0, 0, 2)), True)}


def test_nbt_roundtrip_and_real_schematic_mismatch(house, work):
    path = work / 'inventory and spaces.litematic'
    export_scene(house, path)
    restored = load_scene(path)
    assert semantic_digest(restored) == semantic_digest(house)
    assert {p: c.nbt for p, c in restored.blocks.items() if c.nbt} == {p: c.nbt for p, c in house.blocks.items() if c.nbt}
    _, _, tag, nbtfile = _mcio()
    nbt = nbtfile.load_regardless_of_gzipped(filename=path)
    region = next(iter(nbt['Regions'].values()))
    region['TileEntities'][0]['Items'][0]['count'] = tag.Int(7)
    nbtfile(nbt).save(path)
    with pytest.raises(ContractError, match='geometry-mismatch'):
        load_scene(path)
    companion_path(path).unlink()
    with pytest.raises(ContractError, match='untrusted-structure'):
        load_scene(path)


@pytest.mark.parametrize('environment_seed', range(16))
@pytest.mark.parametrize('building_seed', range(4))
def test_64_frozen_independent_pairings(environment_seed, building_seed):
    field, water = adaptation_expression(environment_seed)
    domain = Box((-8, -4, -18), (34, 64, 34))
    s = Scene(building_seed, domain=domain)
    s.place('ground', Terrain(domain, field, water))
    before = {p: c.state for p, c in s.blocks.items()}
    # An existing protected environmental observation outside all building edits.
    protected = (32, s.view.elevation(32, 32), 32)
    from hearth.components.primitives import Assembly
    s.place('preserved', Assembly('protected-soil', Box(protected, protected), (), (Rule('expected', (protected,), data={'names': [name_of(s.blocks[protected].state)]}),)))
    design = PROGRAMS['dwelling'](building_seed)
    s.place('house', Building(design, '/ground'))
    s.finalize()
    assert s.nodes['/house'].contract.decisions['rooms'] == [(r.x, r.z, r.level, r.purpose) for r in design.normalized()]
    allowed = set()
    for path, old in s.displaced.items():
        if path.startswith('/house/'):
            allowed.update(old)
            assert all(s.nodes[path].contract.envelope.contains(p) for p in old)
    assert all(p in allowed or (p in s.blocks and s.blocks[p].state == material) for p, material in before.items())
    assert s.blocks[protected].state == before[protected]
    assert s.nodes['/house/approach'].contract.decisions['rows']
    assert all(s.view.supports(p) for p in s.nodes['/house/foundation'].contract.rules[0].cells)


def test_environment_implementation_substitution_and_public_forwarding():

    @dataclass(frozen=True)
    class IndependentField:

        def __call__(self, x, z):
            return 4 + 0.01 * x - 0.01 * z

        def expression(self):
            return {'polynomial': [4, .01, -.01]}

    @dataclass(frozen=True)
    class WrappedEnvironment:
        domain: Box

        def capability(self):
            return Capability('extension.environment', ('environment', 'composite'))

        def negotiate(self, c, p):
            ports = tuple(Port(key, key, Frame(self.domain.lo), self.domain, 100000, delegate=('strata', key)) for key in ('construction', 'planting'))
            return Contract(self.domain, ports=ports)

        def realize(self, c, b):
            return Plan(children=[Child('strata', Terrain(self.domain, IndependentField()))])

    domain = Box((-8, -4, -18), (30, 50, 30))
    s = Scene(2, domain=domain)
    s.place('world', WrappedEnvironment(domain))
    s.place('house', Building(Blueprint((RoomSpec(0, 0, 0, 'living'),)), '/world'))
    s.finalize()
    assert s.view.port('/house', 'street').delegate == ('approach', 'street')
    assert any(d['source'] == '/world' for d in s.dependencies)


def test_environment_dependency_regeneration_is_explicit_and_atomic():
    domain = Box((-8, -4, -18), (28, 55, 28))
    s = Scene(3, domain=domain)
    s.place('land', Terrain(domain, constant(3)))
    design = Blueprint((RoomSpec(0, 0, 0, 'living'), RoomSpec(0, 0, 1, 'bedroom')))
    s.place('house', Building(design, '/land'))
    before = semantic_digest(s)
    with pytest.raises(ContractError):
        s.regenerate('/land', Terrain(domain, constant(4)))
    assert semantic_digest(s) == before
    old_floor = s.nodes['/house'].contract.decisions['floor']
    s.regenerate('/land', Terrain(domain, constant(4)), dependents=('/house',))
    assert s.nodes['/house'].contract.decisions['floor'] == old_floor + 1
    assert not any(n.stale for n in s.nodes.values())
    assert all(p in s.blocks for cells in s.reverse.values() for p in cells)


@pytest.mark.parametrize('program', tuple(PROGRAMS))
@pytest.mark.parametrize('seed', range(8))
def test_three_composition_programs(program, seed):
    s = building_scene(PROGRAMS[program](seed), seed)
    assert s.completed and any(n.type.startswith('room.') for n in s.nodes.values())


@pytest.mark.parametrize('seed,count', [(i, None) for i in range(8)] + [(0, 3), (0, 6)])
def test_settlement_corpus_and_boundaries(seed, count):
    s = settlement_scene(seed, count)
    assert s.completed
    buildings = [n for n in s.nodes.values() if n.type == 'building.rooms']
    assert len(buildings) == count if count else 3 <= len(buildings) <= 6
    route = next(n for n in s.nodes.values() if n.type == 'settlement.connections')
    assert len(route.contract.rules[0].cells) == len(buildings) + 2


def test_blockstate_explicit_semantics_and_rotation():
    assert blockstates_equivalent('minecraft:oak_log[axis=x]', 'minecraft:oak_log[axis=x]')
    assert not blockstates_equivalent('minecraft:oak_log', 'minecraft:oak_log[axis=y]')
    assert not blockstates_equivalent('minecraft:oak_log[axis=x]', 'minecraft:oak_log[axis=z]')
    assert not blockstates_equivalent('minecraft:oak_log[axis=x]', 'minecraft:spruce_log[axis=x]')
    with pytest.raises(ValueError):
        validate_state('minecraft:stone[facing=north]')
    for turn in range(4):
        validate_state(Frame(turn=turn).state('minecraft:rail[shape=north_east,waterlogged=false]'))


def test_complete_settlement_transforms_and_public_gateway():
    from hearth.programs import Settlement, dwelling, atelier
    local = Box((-49, -4, -23), (49, 64, 66))
    frame = Frame((150, 2, 80), 1)
    s = Scene(0, domain=frame.box(local))
    s.place('land', Terrain(s.domain, constant(4)))
    s.place('district', Settlement((dwelling(1), atelier(1), dwelling(4)), local, '/land'), frame=frame)
    s.finalize()
    ports = s.view.offers('/district', 'public-access')
    assert len(ports) == 1 and ports[0].frame.turn == 1
    assert ports[0].frame.origin == frame.point((0, 3, -12))
    assert s.view.capability('/district/plot-0').inputs['design.bay'].accepts(9)
    assert any(d['reason'] == 'observed-spatial-input' for d in s.dependencies)


def test_actual_subsurface_support_not_just_height():
    domain = Box((-8, -4, -18), (28, 55, 28))
    s = Scene(0, domain=domain)
    s.place('ground', Terrain(domain, constant(4)))
    s.place('local-cavity', Volume(Box((2, 3, 2), (2, 3, 2)), state_of('air'), 'siteworks'), bindings=(Binding('/ground', 'construction', 'excavate'),))
    assert s.view.elevation(2, 2) == 4 and s.view.sample(2, 2).support_depth == 1
    before = semantic_digest(s)
    with pytest.raises(ContractError, match='substrate-depth'):
        s.place('house', Building(Blueprint((RoomSpec(0, 0, 0, 'living'),)), '/ground'))
    assert semantic_digest(s) == before


def test_path_endpoint_respects_host_authority():
    from hearth.components.landscape import PathNetwork
    from hearth.components.primitives import Assembly
    domain = Box((-4, -3, -4), (12, 20, 12))
    s = Scene(0, domain=domain)
    s.place('ground', Terrain(domain, constant(0)))
    # An unrelated solid host behind the offered endpoint must survive path widening.
    s.place('behind', Volume(Box((-1, 1, -1), (-1, 1, 1)), state_of('stone')))
    s.place('route', PathNetwork(((0, 1, 0), (8, 1, 0)), ((0, 1),)), bindings=(Binding('/ground', 'construction', 'surface'),))
    assert s.cells('/behind') == {(-1, 1, -1), (-1, 1, 0), (-1, 1, 1)}
    s.finalize()


def test_upper_room_bay_boundary_with_vertical_circulation():
    design = Blueprint((RoomSpec(0, 0, 0, 'living'), RoomSpec(0, 0, 1, 'bedroom')), bay=13)
    s = building_scene(design, 5, gradient(base=4), 6)
    assert s.completed and s.nodes['/building'].contract.decisions['bay'] == 13
    assert s.nodes['/building/room-0-0-0/stairs'].contract.rules
