# Roof-form and fitted-room contracts, stateful sources, preserved consumers and replay.
from dataclasses import dataclass, replace
import pytest
from hearth import Scene, Box, Frame, ContractError
from hearth.environment import Terrain, constant
from hearth.components import Blueprint, RoomSpec, Building, InteriorDesign, Hearth, DiningSet
from hearth.components.roofcraft import RoofDesign
from hearth.components.furnishing import FurnishingLayout
from hearth.components.primitives import Volume
from hearth.components.roomlayout import FittedInterior
from hearth.lodges import lodge_scene
from hearth.persistence import export_scene, load_scene, semantic_digest
from hearth.kernel import validate
from hearth.blocks import state_of, name_of, parse, emitted_light, slab
from test.test_building_quality import damaged, snapshot


def craft_design(bay=11, axis='z', hips=(2, 2)):
    return Blueprint((RoomSpec(0, 0, 0, 'living'), RoomSpec(1, 0, 0, 'kitchen')), bay=bay, roof_axis=axis, roof_design=RoofDesign((1, 2), 8, end_hips=hips), attic_purpose='bedroom', porch_bays=2, interior_design=InteriorDesign())


@pytest.mark.parametrize('bay', (9, 11, 13))
@pytest.mark.parametrize('axis', ('x', 'z'))
@pytest.mark.parametrize('hips', ((0, 2), (2, 2)))
def test_half_hips_keep_enclosure_actual_roof_cover_and_loft_access(bay, axis, hips):
    scene = lodge_scene(craft_design(bay, axis, hips), 3, 0, garden=False)
    assert not validate(scene, True)
    for node in scene.nodes.values():
        if node.type == 'roof.weather-cover':
            assert tuple(node.contract.decisions['end_hips']) == hips
            local_box = Box.enclosing(node.frame.inverse(q) for q in node.contract.envelope.corners())
            surface = RoofDesign((1, 2), end_hips=hips).surface(bay, local_box.size[2] - 2)
            p = node.frame.point((bay // 2, surface.height(bay // 2, 0), 0))
            assert scene.view.supports(p)
    assert any(n.type == 'room.loft.bedroom' for n in scene.nodes.values())


@pytest.mark.parametrize('turn', range(4))
def test_craft_rotation_complete_queries_and_nbt_reload(turn, work):
    frame = Frame((30, 0, -25), turn)
    domain = frame.box(Box((-8, -3, -18), (34, 64, 30)))
    s = Scene(5, domain=domain)
    s.place('land', Terrain(domain, constant(2)))
    s.place('building', Building(craft_design(13), '/land'), frame=frame)
    s.finalize()
    hearth = next(n for n in s.nodes.values() if n.type == 'furnishing.hearth')
    q = hearth.frame.point((1, 1, 1))
    assert emitted_light(s.view.state(q)) == 15
    assert s.inspect(q)['chain'][0]['id'] == hearth.path
    assert s.inspect(q)['chain'][-1]['id'] == '/building'
    path = work / 'hipped furnished house.litematic'
    export_scene(s, path)
    from hearth.quality_assessment import audit_pair
    assert audit_pair(s, path)['nodes'] == len(s.nodes)
    restored = load_scene(path)
    assert restored.inspect(q) == s.inspect(q)
    assert restored.cells('/building', True) == s.cells('/building', True)


@pytest.mark.parametrize('purpose', ('living', 'bedroom', 'library', 'kitchen', 'workshop', 'storage'))
def test_fitted_room_purpose_interaction_routes_and_minimum(purpose):
    d = replace(craft_design(13), rooms=(RoomSpec(0, 0, 0, purpose),), porch_bays=1)
    s = lodge_scene(d, 2, 0, garden=False)
    room = s.nodes['/building/room-0-0-0/furniture']
    assert 2 <= room.contract.decisions['stations'] <= 5
    assert any(n.type == 'furnishing.group.' + purpose for n in s.nodes.values())
    assert not validate(s, True)
    portal = next(port for n in s.nodes.values() if n.path.startswith(room.path) for port in s.view.offers(n.path, 'interaction'))
    before = snapshot(s)
    with pytest.raises(ContractError, match='clearance|reachability'):
        head = Box((portal.region.lo[0], portal.region.hi[1], portal.region.lo[2]), portal.region.hi)
        s.place('block-apron', Volume(head, state_of('stone')))
    assert before == snapshot(s)


def station_scene(component):
    s = Scene(1, domain=Box((-4, -2, -4), (12, 14, 12)))
    s.place('land', Terrain(s.domain, constant(0)))
    s.place('station', component, frame=Frame((1, 1, 1)))
    return s.finalize()


def test_hearth_off_state_missing_mantle_and_substrate_are_detected():
    s = station_scene(Hearth())
    fire = (2, 2, 2)
    name, properties = parse(s.view.state(fire))
    properties['lit'] = 'false'
    off = state_of(name, **properties)
    assert emitted_light(off) == 0
    assert emitted_light(s.view.state(fire)) == 15
    assert 'lighting' in {d.rule for d in validate(damaged(s, fire, off), True)}
    assert 'expected-geometry' in {d.rule for d in validate(damaged(s, (1, 3, 1)), True)}
    assert 'support' in {d.rule for d in validate(damaged(s, (1, 0, 1)), True)}


def test_dining_whole_object_removal_and_clearance_rollback():
    s = station_scene(DiningSet(4))
    before = snapshot(s)
    with pytest.raises(ContractError):
        s.place('collision', Volume(Box((1, 1, 1), (1, 1, 1)), s.view.state((1, 1, 1))))
    assert snapshot(s) == before
    assert 'expected-geometry' in {d.rule for d in validate(damaged(s, (1, 1, 1)), True)}
    old = s.cells('/station')
    s.remove('/station')
    assert '/station' not in s.nodes and not s.cells('/station')
    assert all(s.inspect(p)['instance'] is None for p in old)


def test_hip_skin_damage_and_principal_form_change():
    a = lodge_scene(craft_design(11, hips=(0, 0)), 3, 0, garden=False)
    b = lodge_scene(craft_design(11), 3, 0, garden=False)
    from hearth.quality_assessment import principal_signature
    assert principal_signature(a)[0] != principal_signature(b)[0]
    cover = next(n for n in b.nodes.values() if n.type == 'roof.weather-cover')
    rule = next(r for r in cover.contract.rules if r.kind == 'vertical-cover')
    x, y, z = rule.cells[len(rule.cells) // 2]
    broken = b
    for yy in range(y, rule.box.hi[1] + 1):
        broken = damaged(broken, (x, yy, z))
    assert 'roof-cover' in {d.rule for d in validate(broken, True)}


@pytest.mark.parametrize('hips', ((4, 0), (-1, 0), (1,), (True, 1), (3, 3)))
def test_explicit_incompatible_hip_depths_are_not_clamped(hips):
    with pytest.raises(ValueError):
        RoofDesign((1,), end_hips=hips).surface(9, 9)


@dataclass(frozen=True)
class IndependentInteriorPolicy:

    def component(self, purpose, width, depth, stair):
        return FurnishingLayout(purpose, width, depth, stair)


def test_parent_substitutes_interior_policy_and_regenerates_without_stale_owners():
    d = craft_design(11)
    s = lodge_scene(d, 4, 0, garden=False)
    old = {n.path for n in s.nodes.values() if n.type in ('furnishing.hearth', 'furnishing.textiles')}
    assert old
    replacement = replace(d, interior_design=IndependentInteriorPolicy())
    s.regenerate('/building', Building(replacement, '/land'))
    retired = old - set(s.nodes)
    assert retired and all(not s.cells(p, True) for p in retired)
    assert not any(c.owner in retired for c in s.blocks.values())
    assert not any(n.type in ('furnishing.hearth', 'furnishing.textiles') for n in s.nodes.values())
    assert semantic_digest(s) == semantic_digest(lodge_scene(replacement, 4, 0, garden=False))
    assert not validate(s, True)
    assert any(n.type.startswith('furnishing.group.') for n in s.nodes.values())


def test_explicit_station_count_failure_rolls_back_composition():
    s = Scene(2, domain=Box((-2, -2, -2), (14, 20, 14)))
    s.place('land', Terrain(s.domain, constant(0)))
    before = snapshot(s)
    with pytest.raises(ContractError, match='furnishing-count'):
        s.place('impossible', FittedInterior('living', 9, 9, True, InteriorDesign(5, 5)), frame=Frame((0, 1, 0)))
    assert snapshot(s) == before


@pytest.mark.parametrize('bay', (9, 11, 13))
def test_compact_garden_respects_understory_without_dropping_tree_minimum(bay):
    d = replace(craft_design(bay), rooms=(RoomSpec(0, 0, 0, 'living'),), porch_bays=1)
    s = lodge_scene(d, 2, 0)
    assert sum(n.type == 'plant.conifer' for n in s.nodes.values()) >= 3
    assert sum(n.type == 'garden.raised-bed' for n in s.nodes.values()) == 4
    assert not validate(s, True)


@pytest.mark.parametrize('seed', range(8))
def test_new_hearth_client_joins_preserve_locked_graph(seed):
    from examples.hearth_house import hearth_house
    design = hearth_house(seed)
    scene = lodge_scene(design, seed, seed, garden=False)
    assert scene.nodes['/building'].contract.decisions['rooms'] == [(r.x, r.z, r.level, r.purpose) for r in design.normalized()]
    assert any(n.type == 'furnishing.hearth' for n in scene.nodes.values())
    assert all(n.contract.decisions['stations'] >= 2 for n in scene.nodes.values() if n.type.startswith('furniture.fitted.'))
    assert not validate(scene, True)


def test_interior_substitution_preserves_principal_architecture():
    from hearth.quality_assessment import principal_signature
    design = craft_design(11)
    fitted = lodge_scene(design, 3, 0, garden=False)
    classic = lodge_scene(replace(design, interior_design=IndependentInteriorPolicy()), 3, 0, garden=False)
    assert principal_signature(fitted) == principal_signature(classic)
    assert semantic_digest(fitted) != semantic_digest(classic)


@pytest.mark.parametrize('hips', ((0, 0), (2, 2)))
def test_ridge_finials_have_full_height_bases_and_reject_half_slab_gaps(hips):
    scene = lodge_scene(craft_design(11, hips=hips), 3, 0, garden=False)
    finials = [p for n in scene.nodes.values() if n.type == 'roof.weather-cover' for p in scene.cells(n.path) if name_of(scene.view.state(p)) == 'dark_oak_fence']
    assert finials
    for x, y, z in finials:
        base = (x, y - 1, z)
        assert name_of(scene.view.state(base)) == 'dark_oak_planks'
        broken = damaged(scene, base, slab('dark_oak', 'bottom'))
        assert 'expected-geometry' in {d.rule for d in validate(broken, True)}
