import itertools
from itertools import groupby

shoppers = ["Jane", "Joe", "Sarah"]
items = [
    ("lettuce", "greens"),
    ("tomatoes", "greens"),
    ("cucumber", "greens"),
    ("chicken breast", "meats & fish"),
    ("salmon", "meats & fish"),
    ("ice cream", "frozen items"),
]

carts = {shopper: [] for shopper in shoppers}


def collect_shop_items(shopper, items):
    # Imagine this an expensive database query or calculation that is
    # advantageous to batch.
    carts[shopper] += items


# Invoking the `groupby` function directly
for _section, section_items in groupby(items, key=lambda p: p[1]):
    for shopper in shoppers:
        shopper = shopper.title()
        collect_shop_items(shopper, section_items)  # B031
    # We're outside the nested loop and used the group again.
    collect_shop_items(shopper, section_items)  # B031

for _section, section_items in groupby(items, key=lambda p: p[1]):
    collect_shop_items("Jane", section_items)
    collect_shop_items("Joe", section_items)  # B031


# Make sure to detect in other loop constructs as well - `while` loop
for _section, section_items in groupby(items, key=lambda p: p[1]):
    countdown = 3
    while countdown > 0:
        collect_shop_items(shopper, section_items)  # B031
        countdown -= 1

# Make sure to detect in other loop constructs as well - `list` comprehension
collection = []
for _section, section_items in groupby(items, key=lambda p: p[1]):
    collection.append([list(section_items) for _ in range(3)])  # B031

unique_items = set()
another_set = set()
for _section, section_items in groupby(items, key=lambda p: p[1]):
    # For nested loops, it should not flag the usage of the name
    for item in section_items:
        unique_items.add(item)

    # But it should be detected when used again
    for item in section_items:  # B031
        another_set.add(item)

for _section, section_items in groupby(items, key=lambda p: p[1]):
    # Variable has been overridden, skip checking
    section_items = list(unique_items)
    collect_shop_items("Jane", section_items)
    collect_shop_items("Jane", section_items)

for _section, section_items in groupby(items, key=lambda p: p[1]):
    # Variable has been overridden, skip checking
    # Not a realistic situation, just for testing purpose
    (section_items := list(unique_items))
    collect_shop_items("Jane", section_items)
    collect_shop_items("Jane", section_items)

for _section, section_items in groupby(items, key=lambda p: p[1]):
    # This is ok
    collect_shop_items("Jane", section_items)

# Invocation via the `itertools` module
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    for shopper in shoppers:
        collect_shop_items(shopper, section_items)  # B031

for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    _ = [collect_shop_items(shopper, section_items) for shopper in shoppers]  # B031

for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    # The variable is overridden, skip checking.
    _ = [_ for section_items in range(3)]
    _ = [collect_shop_items(shopper, section_items) for shopper in shoppers]

for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    _ = [item for item in section_items]

for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    # The iterator is being used for the second time.
    _ = [(item1, item2) for item1 in section_items for item2 in section_items]  # B031

for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    if _section == "greens":
        collect_shop_items(shopper, section_items)
    else:
        collect_shop_items(shopper, section_items)
        collect_shop_items(shopper, section_items)  # B031

for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    # Mutually exclusive branches shouldn't trigger the warning
    if _section == "greens":
        collect_shop_items(shopper, section_items)
        if _section == "greens":
            collect_shop_items(shopper, section_items)  # B031
        elif _section == "frozen items":
            collect_shop_items(shopper, section_items)  # B031
        else:
            collect_shop_items(shopper, section_items)  # B031
        collect_shop_items(shopper, section_items)  # B031
    elif _section == "frozen items":
        # Mix `match` and `if` statements
        match shopper:
            case "Jane":
                collect_shop_items(shopper, section_items)
                if _section == "fourth":
                    collect_shop_items(shopper, section_items)  # B031
            case _:
                collect_shop_items(shopper, section_items)
    else:
        collect_shop_items(shopper, section_items)
    # Now, it should detect
    collect_shop_items(shopper, section_items)  # B031

for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    # Mutually exclusive branches shouldn't trigger the warning
    match _section:
        case "greens":
            collect_shop_items(shopper, section_items)
            match shopper:
                case "Jane":
                    collect_shop_items(shopper, section_items)  # B031
                case _:
                    collect_shop_items(shopper, section_items)  # B031
        case "frozen items":
            collect_shop_items(shopper, section_items)
            collect_shop_items(shopper, section_items)  # B031
        case _:
            collect_shop_items(shopper, section_items)
    # Now, it should detect
    collect_shop_items(shopper, section_items)  # B031

for group in groupby(items, key=lambda p: p[1]):
    # This is bad, but not detected currently
    collect_shop_items("Jane", group[1])
    collect_shop_items("Joe", group[1])


# https://github.com/astral-sh/ruff/issues/4050
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
    if _section == "greens":
        for item in section_items:
            collect_shop_items(shopper, item)
    elif _section == "frozen items":
        _ = [item for item in section_items]
    else:
        collect_shop_items(shopper, section_items)

#  Make sure we ignore - but don't fail on more complicated invocations
for _key, (_value1, _value2) in groupby(
    [("a", (1, 2)), ("b", (3, 4)), ("a", (5, 6))], key=lambda p: p[1]
):
    collect_shop_items("Jane", group[1])
    collect_shop_items("Joe", group[1])

#  Make sure we ignore - but don't fail on more complicated invocations
for (_key1, _key2), (_value1, _value2) in groupby(
    [(("a", "a"), (1, 2)), (("b", "b"), (3, 4)), (("a", "a"), (5, 6))],
    key=lambda p: p[1],
):
    collect_shop_items("Jane", group[1])
    collect_shop_items("Joe", group[1])

# Shouldn't trigger the warning when there is a continue, break statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
    if _section == "greens":
        collect_shop_items(shopper, section_items)
        continue
    elif _section == "frozen items":
        collect_shop_items(shopper, section_items)
        break
    collect_shop_items(shopper, section_items)

# Shouldn't trigger the warning when there is a return statement.
def foo():
    for _section, section_items in groupby(items, key=lambda p: p[1]):
        if _section == "greens":
            collect_shop_items(shopper, section_items)
            return
        elif _section == "frozen items":
            return section_items
        collect_shop_items(shopper, section_items)

# Should trigger the warning for duplicate access, even if is a return statement after.
def foo():
    from itertools import groupby
    for _section, section_items in groupby(items, key=lambda p: p[1]):
        if _section == "greens":
            collect_shop_items(shopper, section_items)
            collect_shop_items(shopper, section_items)
            return

# Should trigger the warning for duplicate access, even if is a return in another branch.
def foo():
    from itertools import groupby
    for _section, section_items in groupby(items, key=lambda p: p[1]):
        if _section == "greens":
            collect_shop_items(shopper, section_items)
            return
        elif _section == "frozen items":
            collect_shop_items(shopper, section_items)
            collect_shop_items(shopper, section_items)

# Should trigger, since only one branch has a return statement.
def foo():
    from itertools import groupby
    for _section, section_items in groupby(items, key=lambda p: p[1]):
        if _section == "greens":
            collect_shop_items(shopper, section_items)
            return
        elif _section == "frozen items":
            collect_shop_items(shopper, section_items)
        collect_shop_items(shopper, section_items)  # B031

# Let's redefine the `groupby` function to make sure we pick up the correct one.
# NOTE: This should always be at the end of the file.
def groupby(data, key=None):
    pass


for name, group in groupby(items):
    collect_shop_items("Jane", items)
    # This shouldn't be flagged as the `groupby` function is different
    collect_shop_items("Joe", items)
