"""Checks and a report for the blind robot (Part 3).

Run from this folder: python test_blind_robot.py
"""
import random
import time
from types import SimpleNamespace

import blind_robot
import shortest_route_fast
from cleaning_robot import solution_iterative
from test_cleaning_robot import EXAMPLES, floor, random_room
from test_dirt import maze_room

STEP = {'^': (-1, 0), 'v': (1, 0), '<': (0, -1), '>': (0, 1)}
TINY = ['####', '#*.#', '#..#', '####']
RING = ['#####', '#*..#', '#.#.#', '#...#', '#####']
LYING = ['#######', '#*....#', '#######']
STANDING = ['###', '#*#', '#.#', '#.#', '#.#', '#.#', '###']


class Robot:
    """A robot in a real room. The solution only ever gets its move method."""

    def __init__(self, room):
        self._room = room
        self._pos = next((r, c) for r, row in enumerate(room) for c, ch in enumerate(row) if ch == '*')
        self._bumped = set()
        self.moves = self.bumps = self.repeat_bumps = 0
        self.cleaned = {self._pos}
        self.log = []

    def move(self, d):
        if self.moves + self.bumps >= 50_000:
            raise RuntimeError('more than 50,000 move calls')
        n = (self._pos[0] + STEP[d][0], self._pos[1] + STEP[d][1])
        ok = self._room[n[0]][n[1]] != '#'
        self.log.append((d, ok))
        if not ok:
            self.bumps += 1
            self.repeat_bumps += n in self._bumped
            self._bumped.add(n)
            return False
        self._pos = n
        self.moves += 1
        self.cleaned.add(n)
        return True

    def walked(self):
        return ''.join(d for d, ok in self.log if ok)


def walls_touching(room):
    return {(r + dr, c + dc) for r, c in floor(room) for dr, dc in STEP.values()
            if room[r + dr][c + dc] == '#'}


def run(room, solve, **options):
    """Run a solution on a real room and check every guarantee that holds for all variants."""
    robot = Robot(room)
    count = solve(SimpleNamespace(move=robot.move), **options)
    cells = floor(room)
    assert robot.cleaned == cells, f'missed fields in {room}'
    assert robot.bumps == len(walls_touching(room)), f'{robot.bumps} bumps in {room}'
    assert robot.repeat_bumps == 0
    assert count == len(cells)
    return robot


def pad(room):
    """The same room with two extra rows of wall on top and two extra columns on the left."""
    return ['#' * (len(room[0]) + 2)] * 2 + ['##' + row for row in room]


VARIANTS = {
    'plain': (blind_robot.solution, {}),
    'trimmed': (blind_robot.solution_trimmed, {}),
    'trimmed + shortcuts': (blind_robot.solution_trimmed, {'shortcuts': True}),
    'left hand': (blind_robot.solution_trimmed, {'order': blind_robot.left_hand_order}),
    'left hand + shortcuts': (blind_robot.solution_trimmed,
                              {'order': blind_robot.left_hand_order, 'shortcuts': True}),
}


def run_all(room):
    """Every variant on one room, plus the checks that relate them to each other."""
    robots = {name: run(room, solve, **options) for name, (solve, options) in VARIANTS.items()}
    assert robots['plain'].walked() == solution_iterative(room)
    assert robots['plain'].walked().startswith(robots['trimmed'].walked())
    assert robots['trimmed + shortcuts'].moves <= robots['trimmed'].moves
    assert robots['left hand + shortcuts'].moves <= robots['left hand'].moves
    return robots


def rooms_from_post():
    for name, room in (('2x2 room', TINY), ('ring', RING), ('corridor lying', LYING),
                       ('corridor standing', STANDING)):
        robots = run_all(room)
        walking = ', '.join(f'{v} {r.moves}' for v, r in robots.items())
        offline = len(shortest_route_fast.solution(room))
        print(f'  {name:<18} bumps {robots["plain"].bumps:>2} | walking: {walking} | best with a map {offline}')
    assert run(TINY, blind_robot.solution_trimmed).moves == 6
    assert run(TINY, blind_robot.solution_trimmed, order=blind_robot.left_hand_order).walked() == '>v<'
    assert run(LYING, blind_robot.solution_trimmed).moves == 4
    assert run(STANDING, blind_robot.solution_trimmed).moves == 8
    for room, _ in EXAMPLES:
        run_all(room)
    print('rooms from the post ..... ok (and the five example rooms)')


def many_rooms(rng, count=3000, compare_every=10):
    walk = dict.fromkeys(VARIANTS, 0)
    offline_walk = dict.fromkeys(VARIANTS, 0)
    offline_total = bumps = fields = compared = 0
    for i in range(count):
        R, C = rng.randint(3, 40), rng.randint(3, 40)
        room = (maze_room(rng, R, C, '.') if rng.random() < 0.2
                else random_room(rng, R, C, rng.choice([0, 0.1, 0.3, 0.45])))
        robots = run_all(room)
        shortcut = robots['trimmed + shortcuts']
        assert run(pad(room), blind_robot.solution_trimmed, shortcuts=True).log == shortcut.log
        for name, robot in robots.items():
            walk[name] += robot.moves
        bumps += shortcut.bumps
        fields += len(floor(room))
        if i % compare_every == 0:
            compared += 1
            offline_total += len(shortest_route_fast.solution(room))
            for name, robot in robots.items():
                offline_walk[name] += robot.moves
    print(f'random rooms ............ {count} rooms: every variant cleaned everything, bumped every wall '
          f'exactly once ({bumps / fields:.2f} bumps per field)')
    print('  plain walk equals Part 1 command for command; the padded rooms gave identical logs')
    for name in VARIANTS:
        print(f'  {name:<22} {walk[name] / fields:.2f} moves per field, '
              f'{offline_walk[name] / offline_total:.2f}x the best route with a map ({compared} rooms)')


def big_rooms(rng, count=10):
    worst = 0.0
    for _ in range(count):
        room = random_room(rng, 40, 40, rng.choice([0, 0.15, 0.3]))
        t = time.perf_counter()
        run_all(room)
        worst = max(worst, time.perf_counter() - t)
    print(f'big rooms (40x40) ....... {count} rooms, all five variants in {1000 * worst:.0f} ms at worst')


def main():
    rng = random.Random(2026)
    rooms_from_post()
    many_rooms(rng)
    big_rooms(rng)
    print('all checks passed')


if __name__ == '__main__':
    main()
