from unittest.mock import MagicMock, patch

import pytest
from arcade.sdk.errors import RetryableToolError, ToolExecutionError

from arcade_google.models import (
    CellData,
    CellExtendedValue,
    NumberFormatType,
    RowData,
    SheetDataInput,
)
from arcade_google.utils import (
    col_to_index,
    compute_sheet_data_dimensions,
    convert_api_grid_data_to_dict,
    create_cell_data,
    create_row_data,
    create_sheet_data,
    create_sheet_properties,
    extract_user_entered_cell_value,
    group_contiguous_rows,
    index_to_col,
    is_col_greater,
    process_row,
    validate_write_to_cell_params,
)


@pytest.fixture
def sheet_data_input_fixture():
    data = {
        1: {
            "A": "name",
            "B": "age",
            "C": "email",
            "D": "score",
            "E": "gender",
            "F": "city",
            "G": "country",
            "H": "registration_date",
        },
        2: {
            "A": "John Doe",
            "B": 28,
            "C": "johndoe@example.com",
            "D": 85.4,
            "E": "Male",
            "F": "New York",
            "G": "USA",
            "H": "2023-01-15",
        },
        10: {
            "A": "Nate Green",
            "B": 30,
            "C": "nateg@example.com",
            "D": 88,
            "E": "Male",
            "F": "Orlando",
            "G": "USA",
            "H": "2024-02-01",
        },
        43: {
            "A": 100,
            "B": 300,
            "H": 123,
            "I": "=SUM(SEQUENCE(10))",
        },
        44: {
            "A": 456,
            "B": 234,
            "H": 123,
            "I": "=SUM(SEQUENCE(10))",
        },
    }
    return SheetDataInput(data=data)


@pytest.mark.parametrize(
    "col, expected_index",
    [
        ("A", 0),
        ("B", 1),
        ("Z", 25),
        ("AA", 26 + 0),
        ("AZ", (1 * 26) + 25),
        ("BA", (2 * 26) + 0),
        ("ZZ", (26 * 26) + 25),
        ("AAA", (1 * 26 * 26) + (1 * 26) + 0),
        ("AAB", (1 * 26 * 26) + (1 * 26) + 1),
        ("QED", (17 * 26 * 26) + (5 * 26) + 3),
    ],
)
def test_col_to_index(col, expected_index):
    assert col_to_index(col) == expected_index


@pytest.mark.parametrize(
    "index, expected_col",
    [
        (0, "A"),
        (1, "B"),
        (25, "Z"),
        (26 + 0, "AA"),
        ((1 * 26) + 25, "AZ"),
        ((2 * 26) + 0, "BA"),
        ((26 * 26) + 25, "ZZ"),
        ((1 * 26 * 26) + (1 * 26) + 0, "AAA"),
        ((1 * 26 * 26) + (1 * 26) + 1, "AAB"),
        ((17 * 26 * 26) + (5 * 26) + 3, "QED"),
    ],
)
def test_index_to_col(index, expected_col):
    assert index_to_col(index) == expected_col


@pytest.mark.parametrize(
    "col1, col2, expected_result",
    [
        ("A", "B", False),
        ("B", "A", True),
        ("AA", "AB", False),
        ("AB", "AA", True),
        ("A", "AA", False),
        ("AA", "A", True),
        ("Z", "AA", False),
        ("AA", "Z", True),
        ("AAA", "AAB", False),
        ("AAB", "AAA", True),
        ("QED", "QEE", False),
        ("QEE", "QED", True),
    ],
)
def test_is_col_greater(col1, col2, expected_result):
    assert is_col_greater(col1, col2) == expected_result


def test_compute_sheet_data_dimensions(sheet_data_input_fixture):
    (min_row, max_row), (min_col_index, max_col_index) = compute_sheet_data_dimensions(
        sheet_data_input_fixture
    )

    expected_min_row = 1
    expected_max_row = 44
    expected_min_col_index = 0  # Column "A"
    expected_max_col_index = 8  # Column "I"

    assert min_row == expected_min_row
    assert max_row == expected_max_row
    assert min_col_index == expected_min_col_index
    assert max_col_index == expected_max_col_index


def test_create_sheet_properties():
    sheet_properties = create_sheet_properties(
        sheet_id=1,
        title="Sheet1",
        row_count=10000,
        column_count=260,
    )

    assert sheet_properties.sheetId == 1
    assert sheet_properties.title == "Sheet1"
    assert sheet_properties.gridProperties.rowCount == 10000
    assert sheet_properties.gridProperties.columnCount == 260


@pytest.mark.parametrize(
    "row_numbers, expected_groups",
    [
        ([], []),
        ([5, 6, 7], [[5, 6, 7]]),
        (
            [1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 18, 19, 20],
            [[1, 2, 3], [5, 6, 7, 8, 9, 10, 11], [18, 19, 20]],
        ),
    ],
)
def test_group_contiguous_rows(row_numbers, expected_groups):
    grouped_rows = group_contiguous_rows(row_numbers)
    assert grouped_rows == expected_groups


@pytest.mark.parametrize(
    "input_value, expected_key, expected_value, expected_type, expected_pattern",
    [
        (1234, "numberValue", 1234, NumberFormatType.NUMBER, "#,##0"),
        (1.234, "numberValue", 1.234, NumberFormatType.NUMBER, "#,##0.00"),
        ("$100", "numberValue", 100, NumberFormatType.CURRENCY, "$#,##0"),
        ("$100.50", "numberValue", 100.50, NumberFormatType.CURRENCY, "$#,##0.00"),
        ("75%", "numberValue", 75.00, NumberFormatType.PERCENT, "0.00%"),
        ("75.34%", "numberValue", 75.34, NumberFormatType.PERCENT, "0.00%"),
        ("$1abc", "stringValue", "$1abc", None, None),
        ("abc7%", "stringValue", "abc7%", None, None),
        ("=SUM(A1:B1)", "formulaValue", "=SUM(A1:B1)", None, None),
        (True, "boolValue", True, None, None),
    ],
)
def test_create_cell_data(
    input_value, expected_key, expected_value, expected_type, expected_pattern
):
    cell_data = create_cell_data(input_value)
    expected_cell_value = CellExtendedValue(**{expected_key: expected_value})
    assert cell_data.userEnteredValue == expected_cell_value
    if expected_type is None:
        assert cell_data.userEnteredFormat is None
    else:
        assert cell_data.userEnteredFormat is not None
        assert cell_data.userEnteredFormat.numberFormat.type == expected_type
        assert cell_data.userEnteredFormat.numberFormat.pattern == expected_pattern


def test_create_row_data():
    row_data = {
        "A": 1,  # Column index 0
        "B": 2.5,  # Column index 1
        "AA": "test",  # Column index 26
        "BA": True,  # Column index 52
        "BB": "=SUM(A1:B1)",  # Column index 53
    }
    min_col_index = 0  # Column "A"
    max_col_index = 53  # Column "BB"

    expected_row_data = RowData(
        values=[
            CellData(userEnteredValue=CellExtendedValue(stringValue=""))
            for _ in range(max_col_index + 1)
        ]
    )
    expected_row_data.values[0].userEnteredValue = CellExtendedValue(numberValue=1)
    expected_row_data.values[1].userEnteredValue = CellExtendedValue(numberValue=2.5)
    expected_row_data.values[26].userEnteredValue = CellExtendedValue(stringValue="test")
    expected_row_data.values[52].userEnteredValue = CellExtendedValue(boolValue=True)
    expected_row_data.values[53].userEnteredValue = CellExtendedValue(formulaValue="=SUM(A1:B1)")

    row_data = create_row_data(row_data, min_col_index, max_col_index)

    assert len(row_data.values) == len(expected_row_data.values)
    for cell, expected in zip(row_data.values, expected_row_data.values, strict=False):
        assert cell.userEnteredValue == expected.userEnteredValue


def test_create_sheet_data():
    from arcade_google.models import CellData, CellExtendedValue, SheetDataInput
    from arcade_google.utils import create_cell_data

    test_data = {
        2: {"B": "row2B", "C": 200},
        3: {"B": "row3B"},
        5: {"A": "=SUM(A1:A1)", "C": "row5C"},
    }
    sheet_data_input = SheetDataInput(data=test_data)
    min_col_index = 0  # Column "A"
    max_col_index = 2  # Column "C"

    grid_data_list = create_sheet_data(sheet_data_input, min_col_index, max_col_index)

    assert len(grid_data_list) == 2, "Should have two groups of contiguous rows"

    group1 = grid_data_list[0]
    assert group1.startRow == 1
    assert group1.startColumn == min_col_index
    assert len(group1.rowData) == 2

    row2_cells = group1.rowData[0].values
    expected_row2 = [
        CellData(userEnteredValue=CellExtendedValue(stringValue="")),
        create_cell_data("row2B"),
        create_cell_data(200),
    ]
    for cell, expected in zip(row2_cells, expected_row2, strict=False):
        assert cell.userEnteredValue == expected.userEnteredValue

    row3_cells = group1.rowData[1].values
    expected_row3 = [
        CellData(userEnteredValue=CellExtendedValue(stringValue="")),
        create_cell_data("row3B"),
        CellData(userEnteredValue=CellExtendedValue(stringValue="")),
    ]
    for cell, expected in zip(row3_cells, expected_row3, strict=False):
        assert cell.userEnteredValue == expected.userEnteredValue

    group2 = grid_data_list[1]
    assert group2.startRow == 4
    assert group2.startColumn == min_col_index
    assert len(group2.rowData) == 1

    row5_cells = group2.rowData[0].values
    expected_row5 = [
        create_cell_data("=SUM(A1:A1)"),
        CellData(userEnteredValue=CellExtendedValue(stringValue="")),
        create_cell_data("row5C"),
    ]
    for cell, expected in zip(row5_cells, expected_row5, strict=False):
        assert cell.userEnteredValue == expected.userEnteredValue


@pytest.mark.parametrize(
    "cell, expected",
    [
        ({}, ""),
        ({"userEnteredValue": {}}, ""),
        ({"userEnteredValue": {"stringValue": "hello"}}, "hello"),
        ({"userEnteredValue": {"numberValue": 123}}, 123),
        ({"userEnteredValue": {"boolValue": True}}, True),
        ({"userEnteredValue": {"formulaValue": "=SUM(A1:A2)"}}, "=SUM(A1:A2)"),
    ],
)
def test_extract_user_entered_cell_value(cell, expected):
    result = extract_user_entered_cell_value(cell)
    assert result == expected


def test_process_row_empty():
    row = {}
    assert process_row(row, 0) == {}


def test_process_row_non_empty():
    row = {
        "values": [
            {"userEnteredValue": {"stringValue": "cell1"}, "formattedValue": "cell1"},
            {"userEnteredValue": {}},  # should be ignored
            {"userEnteredValue": {"formulaValue": "=C1+D4"}, "formattedValue": 42},
            {"userEnteredValue": {"stringValue": ""}, "formattedValue": ""},  # should be ignored
            {"userEnteredValue": {"boolValue": False}, "formattedValue": False},
        ]
    }
    expected = {
        "A": {"userEnteredValue": "cell1", "formattedValue": "cell1"},
        "C": {"userEnteredValue": "=C1+D4", "formattedValue": 42},
        "E": {"userEnteredValue": False, "formattedValue": False},
    }

    assert process_row(row, 0) == expected


def test_process_row_with_start_index():
    row = {
        "values": [
            {"userEnteredValue": {"stringValue": "x"}, "formattedValue": "x"},
            {"userEnteredValue": {"formulaValue": "=C1+D4"}, "formattedValue": "$10.00"},
        ]
    }
    expected = {
        "C": {"userEnteredValue": "x", "formattedValue": "x"},
        "D": {"userEnteredValue": "=C1+D4", "formattedValue": "$10.00"},
    }

    assert process_row(row, 2) == expected


def test_convert_api_grid_data_to_dict_single_grid():
    data = [
        {
            "startRow": 0,
            "startColumn": 0,
            "rowData": [
                {
                    "values": [
                        {"userEnteredValue": {"stringValue": "A1"}, "formattedValue": "A1"},
                        {"userEnteredValue": {"numberValue": 1}, "formattedValue": 1},
                    ]
                },
                {
                    "values": [
                        {"userEnteredValue": {"stringValue": "A2"}, "formattedValue": "A2"},
                        {"userEnteredValue": {"numberValue": 2}, "formattedValue": 2},
                    ]
                },
                {
                    "values": [
                        {"userEnteredValue": {}},
                        {
                            "userEnteredValue": {"stringValue": "ignored"},
                            "formattedValue": "ignored",
                        },
                        {"userEnteredValue": {"numberValue": 3}, "formattedValue": 3},
                    ]
                },
            ],
        }
    ]
    result = convert_api_grid_data_to_dict(data)
    expected = {
        1: {
            "A": {"userEnteredValue": "A1", "formattedValue": "A1"},
            "B": {"userEnteredValue": 1, "formattedValue": 1},
        },
        2: {
            "A": {"userEnteredValue": "A2", "formattedValue": "A2"},
            "B": {"userEnteredValue": 2, "formattedValue": 2},
        },
        3: {
            "B": {"userEnteredValue": "ignored", "formattedValue": "ignored"},
            "C": {"userEnteredValue": 3, "formattedValue": 3},
        },
    }

    assert result == expected


def test_convert_api_grid_data_to_dict_multiple_grids():
    data = [
        {
            "startRow": 5,
            "startColumn": 1,
            "rowData": [
                {
                    "values": [
                        {"userEnteredValue": {"numberValue": 100}, "formattedValue": 100},
                        {"userEnteredValue": {"stringValue": "=SUM(A1:A2)"}, "formattedValue": 23},
                    ]
                }
            ],
        },
        {
            "startRow": 0,
            "startColumn": 0,
            "rowData": [
                {
                    "values": [
                        {"userEnteredValue": {"stringValue": "First"}, "formattedValue": "First"},
                        {"userEnteredValue": {"numberValue": 10}, "formattedValue": 10},
                    ]
                }
            ],
        },
    ]
    result = convert_api_grid_data_to_dict(data)
    expected = {
        1: {
            "A": {"userEnteredValue": "First", "formattedValue": "First"},
            "B": {"userEnteredValue": 10, "formattedValue": 10},
        },
        6: {
            "B": {"userEnteredValue": 100, "formattedValue": 100},
            "C": {"userEnteredValue": "=SUM(A1:A2)", "formattedValue": 23},
        },
    }

    assert result == expected


def test_convert_api_grid_data_to_dict_empty_rows():
    data = [
        {
            "startRow": 10,
            "startColumn": 0,
            "rowData": [
                {"values": [{"userEnteredValue": {}, "formattedValue": ""}]},
                {"values": []},
            ],
        }
    ]
    result = convert_api_grid_data_to_dict(data)
    expected = {}

    assert result == expected


FAKE_SHEET_RESPONSE = {
    "sheets": [
        {"properties": {"title": "Sheet1", "gridProperties": {"rowCount": 10, "columnCount": 6}}}
    ]
}


@patch("arcade_google.utils.build_sheets_service")
def test_validate_write_to_cell_params_valid(mock_build):
    mock_service = MagicMock()
    mock_service.spreadsheets().get().execute.return_value = FAKE_SHEET_RESPONSE
    mock_build.return_value = mock_service

    service = mock_build("dummy_token")

    validate_write_to_cell_params(
        service=service,
        spreadsheet_id="dummy_id",
        sheet_name="Sheet1",
        column="B",
        row=5,
    )


@patch("arcade_google.utils.build_sheets_service")
def test_validate_write_to_cell_params_invalid_sheet_name(mock_build):
    mock_service = MagicMock()
    mock_service.spreadsheets().get().execute.return_value = FAKE_SHEET_RESPONSE
    mock_build.return_value = mock_service

    service = mock_build("dummy_token")

    with pytest.raises(RetryableToolError) as excinfo:
        validate_write_to_cell_params(
            service=service,
            spreadsheet_id="dummy_id",
            sheet_name="NonExistentSheet",
            column="A",
            row=5,
        )
    assert "Sheet name NonExistentSheet not found" in str(excinfo.value)


@patch("arcade_google.utils.build_sheets_service")
def test_validate_write_to_cell_params_row_out_of_bounds(mock_build):
    mock_service = MagicMock()
    mock_service.spreadsheets().get().execute.return_value = FAKE_SHEET_RESPONSE
    mock_build.return_value = mock_service

    service = mock_build("dummy_token")

    out_of_bounds_row = 15
    with pytest.raises(ToolExecutionError) as excinfo:
        validate_write_to_cell_params(
            service=service,
            spreadsheet_id="dummy_id",
            sheet_name="Sheet1",
            column="A",
            row=out_of_bounds_row,
        )
    assert f"Row {out_of_bounds_row} is out of bounds" in str(excinfo.value)


@patch("arcade_google.utils.build_sheets_service")
def test_validate_write_to_cell_params_column_out_of_bounds(mock_build):
    mock_service = MagicMock()
    mock_service.spreadsheets().get().execute.return_value = FAKE_SHEET_RESPONSE
    mock_build.return_value = mock_service

    service = mock_build("dummy_token")

    out_of_bounds_column = "Z"
    with pytest.raises(ToolExecutionError) as excinfo:
        validate_write_to_cell_params(
            service=service,
            spreadsheet_id="dummy_id",
            sheet_name="Sheet1",
            column=out_of_bounds_column,
            row=5,
        )
    assert f"Column {out_of_bounds_column} is out of bounds" in str(excinfo.value)
