from component import Row, Column, SubGrid


class Puzzle:
    puzzle_count = 0
    guess_count = 0

    @classmethod
    def new_puzzle_trying_guess(cls, puzzle, guess, position=None):
        if position is None:
            position = puzzle.guess_position
        old = puzzle.game
        new = old[:position] + guess + old[position+1:]
        return cls(new)

    @classmethod
    def from_list(cls, a_list):
        joined = ''.join(a_list)
        return cls(joined)

    def __init__(self, a_game_string):
        Puzzle.puzzle_count += 1
        self.game = a_game_string
        if len(a_game_string) == 9:
            self.available_values = '123'
            self.line_size = 3
        elif len(a_game_string) == 81:
            self.available_values = '123456789'
            self.line_size = 9
        else:
            raise ValueError('problem must have length 9 or 81')
        self.guess_position = self.first_unsolved_position()

    @property
    def is_filled_in(self):
        return self.guess_position == -1

    @property
    def columns(self):
        return [Column(self,col) for col in range(0, 9)]

    @property
    def columns_are_valid(self):
        return all(column.is_valid for column in self.columns)

    @property
    def rows(self):
        return [Row(self, row) for row in range(0, 81, 9)]

    @property
    def rows_are_valid(self):
        return all(row.is_valid for row in self.rows)

    @property
    def sub_grids(self):
        return [SubGrid(self, pos) for pos in [0, 3, 6, 27, 30, 33, 54, 57, 60]]

    @property
    def sub_grids_are_valid(self):
        return all(sub_grid.is_valid for sub_grid in self.sub_grids)

    @property
    def is_correctly_solved(self):
        return self.rows_are_valid\
            and self.columns_are_valid\
            and self.sub_grids_are_valid

    def possible_answers(self, position=None):
        if position is None:
            position = self.guess_position
        row = Row(self, position)
        column = Column(self, position)
        sub_grid = SubGrid(self, position)
        return [c for c in self.available_values if c not in row and c not in column and c not in sub_grid]

    def find_next_puzzles(self):
        guesses = self.possible_answers()
        Puzzle.guess_count += len(guesses)
        puzzles = (Puzzle.new_puzzle_trying_guess(self, guess) for guess in guesses)
        return puzzles

    def first_unsolved_position(self):
        try:
            return self.game.index('0')
        except ValueError:
            return -1

from puzzle import Puzzle


class Solver:
    solver_count = 0

    def __init__(self, puzzle):
        Solver.solver_count += 1
        self.puzzle = puzzle

    def solve(self) -> Puzzle | None:
        if self.puzzle.is_filled_in:
            return self.puzzle
        for new_puzzle in self.puzzle.find_next_puzzles():
            solved_puzzle = Solver(new_puzzle).solve()
            if solved_puzzle:
                return solved_puzzle
        return None

class Component():
    def __iter__(self):
        return iter(self.used)

    @property
    def used_numbers(self):
        return [c for c in self.used if c != '0']

    @property
    def is_valid(self):
        for c in '123456789':
            if c not in self.used:
                return False
        return True


class Row(Component):
    def __init__(self, puzzle, position):
        start = position - position % puzzle.line_size
        self.used = puzzle.game[start:start + puzzle.line_size]


class Column(Component):
    def __init__(self, puzzle, position):
        start = position % puzzle.line_size
        self.used = puzzle.game[start:len(puzzle.game):puzzle.line_size]


class SubGrid(Component):
    def __init__(self, puzzle, position):
        starting_row_of_sub_grid = position - position % (3*puzzle.line_size)
        offset_in_row = position % 9 // 3 * 3
        first_index_in_sub_grid = starting_row_of_sub_grid + offset_in_row
        result = []
        if puzzle.line_size == 9:
            for row_index in range(3):
                cell = first_index_in_sub_grid + row_index*9
                result.extend(puzzle.game[cell:cell+3])
        self.used = result



"""
This file contains small tests used to try out small functions such as
functions to access rows or columns or sub-grids. The file is essentially
self-contained, testing nothing but what is here. The tests were used to
answer questions like "How can we easily get the array indices for a column?"
"""


def make_rows():
    rows = []
    for r in range(9):
        rows.append([])
        for c in range(r*9, r*9+9):
            rows[r].append(c)
    return rows


def make_columns():
    columns = []
    for r in range(9):
        columns.append([])
        for c in range(9):
            columns[r].append(r + c*9)
    return columns


def make_grid(grid_index):
    grid = []
    first_row_base = grid_index//3*27
    grid_column = grid_index % 3
    column_addition = grid_column*3
    for r in range(3):
        row_addition = r*9
        for c in range(3):
            grid.append(first_row_base + row_addition + column_addition + c)
    return grid


def column_index(cell_number):
    return cell_number % 9


def row_index(cell_number):
    return cell_number // 9


def pn(cells):
    s = ''
    ct = 0
    for v in cells:
        s += f' {v:2d} |'
        ct += 1
        if ct == 3 or ct == 6:
            s += '|'
    return s


def pretty_print(solution):
    row = 0
    print()
    s = ''
    for start in range(0, 81, 9):
        s += '|' + pn(solution[start:start+9]) + '\n'
        row = row + 1
        if row % 3 == 0:
            s += '||\n'
    print(s)


class TestIdeas:
    def test_setup(self):
        puzzle = '004300209005009001070060043006002087190007400050083000600000105003508690042910300'
        assert len(puzzle) == 81

    def test_make_rows(self):
        rows = make_rows()
        assert rows[8] == [72, 73, 74, 75, 76, 77, 78, 79, 80]

    def test_make_columns(self):
        columns = make_columns()
        assert columns[0] == [0, 9, 18, 27, 36, 45, 54, 63, 72]
        assert columns[8] == [8, 17, 26, 35, 44, 53, 62, 71, 80]

    def test_make_grids_0(self):
        grid = make_grid(0)
        assert grid == [0, 1, 2, 9, 10, 11, 18, 19, 20]

    def test_make_grids_1(self):
        grid = make_grid(1)
        assert grid == [3, 4, 5, 12, 13, 14, 21, 22, 23]

    def test_make_grids_3(self):
        grid = make_grid(3)
        assert grid == [27, 28, 29, 36, 37, 38, 45, 46, 47]

    def test_make_grids_8(self):
        grid = make_grid(8)
        assert grid == [60, 61, 62, 69, 70, 71, 78, 79, 80]

    """
     0  1  2  3  4  5  6  7  8 
     9 10 11 12 13 14 15 16 17 
    18 19 20 21 22 23 24 25 26 
    27 28 29 30 31 32 33 34 35 
    36 37 38 39 40 41 42 43 44 
    45 46 47 48 49 50 51 52 53 
    54 55 56 57 58 59 60 61 62 
    63 64 65 66 67 68 69 70 71 
    72 73 74 75 76 77 78 79 80 
    """

    def test_indexes(self):
        grid = '\n'
        for c in range(9):
            start = c*9
            for r in range(9):
                v = start + r
                v = f"{v:2.0f} "
                grid += v
            grid += '\n'
        # print(grid)
        # assert False

    def test_index_to_column(self):
        assert column_index(0) == 0
        assert column_index(17) == 8
        assert column_index(74) == 2

    def test_index_to_row(self):
        assert row_index(0) == 0
        assert row_index(8) == 0
        assert row_index(17) == 1
        assert row_index(9) == 1
        assert row_index(10) == 1
        assert row_index(72) == 8
        assert row_index(80) == 8

    def test_pos(self):
        def find_zero(a_game):
            return a_game.index('0')

        game = '103000010'
        assert find_zero(game) == 1
        game = '123000010'
        assert find_zero(game) == 3

    def test_column_print(self):
        # cells = [i for i in range(81)]
        # pretty_print(cells)
        # cells = [i % 9 for i in range(81)]
        # pretty_print(cells)
        # cells = [i - i % 9 for i in range(81)]
        # pretty_print(cells)
        # cells = [i - i % 27 for i in range(81)]
        # pretty_print(cells)
        # cells = [(i - i % 27) + (i % 9)//3*3 for i in range(81)]
        # pretty_print(cells)
        assert True

        from puzzle import Puzzle
from component import Row, Column, SubGrid


class TestPuzzle:
    def test_row(self):
        game = '103000010'
        puzzle = Puzzle(game)
        assert Row(puzzle, 0).used_numbers == ['1', '3']
        assert Row(puzzle, 1).used_numbers == ['1', '3']
        assert Row(puzzle, 2).used_numbers == ['1', '3']
        assert Row(puzzle, 3).used_numbers == []
        assert Row(puzzle, 4).used_numbers == []
        assert Row(puzzle, 5).used_numbers == []
        assert Row(puzzle, 6).used_numbers == ['1']
        assert Row(puzzle, 7).used_numbers == ['1']
        assert Row(puzzle, 8).used_numbers == ['1']

    def test_column(self):
        game = '103000010'
        puzzle = Puzzle(game)
        assert Column(puzzle, 1).used_numbers == ['1', ]
        assert Column(puzzle, 4).used_numbers == ['1', ]
        assert Column(puzzle, 7).used_numbers == ['1', ]
        assert Column(puzzle, 0).used_numbers == ['1', ]
        assert Column(puzzle, 3).used_numbers == ['1', ]
        assert Column(puzzle, 6).used_numbers == ['1', ]
        assert Column(puzzle, 2).used_numbers == ['3', ]
        assert Column(puzzle, 5).used_numbers == ['3', ]
        assert Column(puzzle, 8).used_numbers == ['3', ]

    def test_avail(self):
        game = '103000010'
        puzzle = Puzzle(game)
        avail = puzzle.possible_answers(0)
        assert avail == ['2',]
        avail = puzzle.possible_answers(1)
        assert avail == ['2',]
        avail = puzzle.possible_answers(2)
        assert avail == ['2',]
        avail = puzzle.possible_answers(3)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(4)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(5)
        assert avail == ['1', '2']
        avail = puzzle.possible_answers(6)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(7)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(8)
        assert avail == ['2',]

    def test_avail_in_proposed_solvable_game(self):
        puzzle = Puzzle('103000001')
        assert puzzle.possible_answers(1) == ['2', ]
        assert puzzle.possible_answers(3) == ['2', '3']

    def test_first_available_position(self):
        puzzle = Puzzle('123102103')
        assert puzzle.first_unsolved_position() == 4
        puzzle = Puzzle('123312312')
        assert puzzle.first_unsolved_position() == -1

    def test_we_can_replace_value_at_one(self):
        puzzle = Puzzle('aXcdefghi')
        repl = Puzzle.new_puzzle_trying_guess(puzzle, 'b', 1)
        assert repl.game == 'abcdefghi'

    def test_we_can_replace_value_at_zero(self):
        puzzle = Puzzle('Xbcdefghi')
        repl = Puzzle.new_puzzle_trying_guess(puzzle, 'a', 0)
        assert repl.game == 'abcdefghi'

    def test_we_can_replace_value_at_end(self):
        puzzle = Puzzle('abcdefghX')
        repl = Puzzle.new_puzzle_trying_guess(puzzle, 'i', 8)
        assert repl.game == 'abcdefghi'

    def test_used_in_row(self):
        puzzle_lines = [
            '123456780',
            '023456789',
            '123400789',
            '123456789',
            '123456789',
            '123456789',
            '123456789',
            '123456789',
            '123456789',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        used = Row(puzzle, 4).used_numbers
        assert used == ['1', '2', '3', '4', '5', '6', '7', '8']
        used = Row(puzzle, 13).used_numbers
        assert used == ['2', '3', '4', '5', '6', '7', '8', '9']
        used = Row(puzzle, 22).used_numbers
        assert used == ['1', '2', '3', '4', '7', '8', '9']

    def test_used_in_column(self):
        puzzle_lines = [
            '023456789',
            '103456798',
            '203456987',
            '345678910',
            '453456785',
            '563456784',
            '673456783',
            '783456782',
            '893456781',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        used = Column(puzzle, 9).used_numbers  # first column
        assert used == ['1', '2', '3', '4', '5', '6', '7', '8']
        used = Column(puzzle, 19).used_numbers  # second column
        assert used == ['2', '4', '5', '6', '7', '8', '9']
        used = Column(puzzle, 26).used_numbers  # last column
        assert used == ['9', '8', '7', '5', '4', '3', '2', '1']
        
    def test_grid_containing_0(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        ones = SubGrid(puzzle, 0).used_numbers
        assert sum((int(c) for c in ones)) == 9
        for pos in [0, 1, 2, 9, 10, 11, 18, 19, 20]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == ones

    def test_grid_containing_3(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        twos = SubGrid(puzzle, 3).used_numbers
        # print(f'{twos=}')
        assert sum((int(c) for c in twos)) == 18
        for pos in [3, 4, 5, 12, 13, 14, 21, 22, 23]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == twos

    def test_grid_containing_42(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        res = SubGrid(puzzle, 42).used_numbers
        # print(f'{res=}')
        assert sum((int(c) for c in res)) == 54
        for pos in [42, 43, 44, 51, 52, 53, 60, 61, 62]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == res

    def test_grid_containing_75(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        res = SubGrid(puzzle, 75).used_numbers
        # print(f'{res=}')
        assert sum((int(c) for c in res)) == 72
        for pos in [57, 58, 59, 66, 67, 68, 75, 76, 77]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == res

    def test_different_grid_containing_0(self):
        """belt and suspenders. just felt nervous."""
        puzzle_lines = [
            '123222333',
            '456222333',
            '789222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        one_thru_nine = SubGrid(puzzle, 0).used_numbers
        assert sum(int(c) for c in one_thru_nine) == 45

from puzzle import Puzzle
from component import Row, Column, SubGrid


class TestPuzzle:
    def test_row(self):
        game = '103000010'
        puzzle = Puzzle(game)
        assert Row(puzzle, 0).used_numbers == ['1', '3']
        assert Row(puzzle, 1).used_numbers == ['1', '3']
        assert Row(puzzle, 2).used_numbers == ['1', '3']
        assert Row(puzzle, 3).used_numbers == []
        assert Row(puzzle, 4).used_numbers == []
        assert Row(puzzle, 5).used_numbers == []
        assert Row(puzzle, 6).used_numbers == ['1']
        assert Row(puzzle, 7).used_numbers == ['1']
        assert Row(puzzle, 8).used_numbers == ['1']

    def test_column(self):
        game = '103000010'
        puzzle = Puzzle(game)
        assert Column(puzzle, 1).used_numbers == ['1', ]
        assert Column(puzzle, 4).used_numbers == ['1', ]
        assert Column(puzzle, 7).used_numbers == ['1', ]
        assert Column(puzzle, 0).used_numbers == ['1', ]
        assert Column(puzzle, 3).used_numbers == ['1', ]
        assert Column(puzzle, 6).used_numbers == ['1', ]
        assert Column(puzzle, 2).used_numbers == ['3', ]
        assert Column(puzzle, 5).used_numbers == ['3', ]
        assert Column(puzzle, 8).used_numbers == ['3', ]

    def test_avail(self):
        game = '103000010'
        puzzle = Puzzle(game)
        avail = puzzle.possible_answers(0)
        assert avail == ['2',]
        avail = puzzle.possible_answers(1)
        assert avail == ['2',]
        avail = puzzle.possible_answers(2)
        assert avail == ['2',]
        avail = puzzle.possible_answers(3)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(4)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(5)
        assert avail == ['1', '2']
        avail = puzzle.possible_answers(6)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(7)
        assert avail == ['2', '3']
        avail = puzzle.possible_answers(8)
        assert avail == ['2',]

    def test_avail_in_proposed_solvable_game(self):
        puzzle = Puzzle('103000001')
        assert puzzle.possible_answers(1) == ['2', ]
        assert puzzle.possible_answers(3) == ['2', '3']

    def test_first_available_position(self):
        puzzle = Puzzle('123102103')
        assert puzzle.first_unsolved_position() == 4
        puzzle = Puzzle('123312312')
        assert puzzle.first_unsolved_position() == -1

    def test_we_can_replace_value_at_one(self):
        puzzle = Puzzle('aXcdefghi')
        repl = Puzzle.new_puzzle_trying_guess(puzzle, 'b', 1)
        assert repl.game == 'abcdefghi'

    def test_we_can_replace_value_at_zero(self):
        puzzle = Puzzle('Xbcdefghi')
        repl = Puzzle.new_puzzle_trying_guess(puzzle, 'a', 0)
        assert repl.game == 'abcdefghi'

    def test_we_can_replace_value_at_end(self):
        puzzle = Puzzle('abcdefghX')
        repl = Puzzle.new_puzzle_trying_guess(puzzle, 'i', 8)
        assert repl.game == 'abcdefghi'

    def test_used_in_row(self):
        puzzle_lines = [
            '123456780',
            '023456789',
            '123400789',
            '123456789',
            '123456789',
            '123456789',
            '123456789',
            '123456789',
            '123456789',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        used = Row(puzzle, 4).used_numbers
        assert used == ['1', '2', '3', '4', '5', '6', '7', '8']
        used = Row(puzzle, 13).used_numbers
        assert used == ['2', '3', '4', '5', '6', '7', '8', '9']
        used = Row(puzzle, 22).used_numbers
        assert used == ['1', '2', '3', '4', '7', '8', '9']

    def test_used_in_column(self):
        puzzle_lines = [
            '023456789',
            '103456798',
            '203456987',
            '345678910',
            '453456785',
            '563456784',
            '673456783',
            '783456782',
            '893456781',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        used = Column(puzzle, 9).used_numbers  # first column
        assert used == ['1', '2', '3', '4', '5', '6', '7', '8']
        used = Column(puzzle, 19).used_numbers  # second column
        assert used == ['2', '4', '5', '6', '7', '8', '9']
        used = Column(puzzle, 26).used_numbers  # last column
        assert used == ['9', '8', '7', '5', '4', '3', '2', '1']
        
    def test_grid_containing_0(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        ones = SubGrid(puzzle, 0).used_numbers
        assert sum((int(c) for c in ones)) == 9
        for pos in [0, 1, 2, 9, 10, 11, 18, 19, 20]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == ones

    def test_grid_containing_3(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        twos = SubGrid(puzzle, 3).used_numbers
        # print(f'{twos=}')
        assert sum((int(c) for c in twos)) == 18
        for pos in [3, 4, 5, 12, 13, 14, 21, 22, 23]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == twos

    def test_grid_containing_42(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        res = SubGrid(puzzle, 42).used_numbers
        # print(f'{res=}')
        assert sum((int(c) for c in res)) == 54
        for pos in [42, 43, 44, 51, 52, 53, 60, 61, 62]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == res

    def test_grid_containing_75(self):
        puzzle_lines = [
            '111222333',
            '111222333',
            '111222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        res = SubGrid(puzzle, 75).used_numbers
        # print(f'{res=}')
        assert sum((int(c) for c in res)) == 72
        for pos in [57, 58, 59, 66, 67, 68, 75, 76, 77]:
            res = SubGrid(puzzle, pos).used_numbers
            assert res == res

    def test_different_grid_containing_0(self):
        """belt and suspenders. just felt nervous."""
        puzzle_lines = [
            '123222333',
            '456222333',
            '789222333',
            '444555666',
            '444555666',
            '444555666',
            '777888999',
            '777888999',
            '777888999',
        ]
        puzzle = Puzzle.from_list(puzzle_lines)
        one_thru_nine = SubGrid(puzzle, 0).used_numbers
        assert sum(int(c) for c in one_thru_nine) == 45