import requests #для запроса к API
import xml.etree.ElementTree #для обработки xml-ответа API
import matplotlib.pyplot as plt #для построения графиков
import pandas as pd #для создания датафрейма и разденеия всех свечей на два типа: close и open
import datetime #для дат по оси иксов
import pickle #для хранения переменных в файле
import json #для работы с датабазой
import aiofiles #асинхронная работа с файлами
import aiohttp #асинхронные http-запросы

#нет проблем с инструментами теханализа и пустыми днями (без торгов), тк дни без торгов в датафрейме не нули, а просто не существуют. Поэтому они не портят значения индикаторов
#класс тикер, методы график и тек. цена
class ticker():
    """Тикер акции и всё с ним связанное, через MoexApi \n
    Требуются библеотеки: \n
    requests \n
    xml.etree.ElementTree \n
    matplotlib.pyplot as plt \n
    pandas as pd \n
    datetime \n
    pickle \n
    json \n
    """
    def __init__(self, name: str):
        """self.name - имя тикера
        self.tech_dict - словарь теханализа"""
        self.name = name
        """Имя тикера, то есть сам по себе тикер"""
        #в принципе тут можно менять общие для всех юзеров настройки по умолчанию. Потенциально надо через это реализовать кастомные инструменты теханализа
        self.tech_dict = {"value" : {"use" : False, "has_periods" : False, "need_graph_space" : True},
                          "sma" : {"use" : False, "has_periods" : True, "periods" : [], "need_graph_space" : False},
                          "ema" : {"use" : False, "has_periods" : True, "periods" : [],"need_graph_space" : False}
                          }
        """Словарь реализованных опций теханализа. Имеет вид \n
        {"sma": {"use": True, "periods": [20, 50], "need_graph_space": False}, "rsi": {"use": True, "periods": [10], "need_graph_space": True}} \n
        Где use отвечает за использование, period - список периодов, по которым будут считаться значения, need_graph_space за то, требует ли осциллятор доп места на графике \n
        Изначально все use имеют значение False, а списки периодов пусты \n \n
        При реализации нового инструмента теханализа достаточно дописать его в self.tech_dict \n
        При этом функцию, соответствующую этому инструменту важно назвать также, как и сам инструмент в словаре. А её аргументы - self и ax (редактируемый/заполняемый график) \n
        Доп графики инструментов теханализа, которые их требуют, будут отображаться в таком же порядке, в котором инструменты располагаются в словаре. Также в этом порядке будут высвечиваться кнопки в боте и уже выбранные инструменты теханализа"""
    async def correct_name(self):
        """Проверка имени тикера на наличие в множестве тикеров. Множество обновляется не чаще раза в день"""
        async with aiofiles.open(r"D:\MoexAPI_bot_aiogram3\data_files\Info.json", "r", encoding="utf-8") as info_opened_file:
            info = json.loads(await info_opened_file.read())
        if datetime.datetime.now() - datetime.timedelta(days=1) > datetime.datetime.strptime(info["last_day_check"]["ticker"], "%Y-%m-%d %H:%M:%S.%f"): #проверяем условие что дата перезаписи списка тикеров это хотя бы 1 день назад
            #если отличается более чем на 1 день, то переписываем список (множество) тикеров:
            set_tickers = set() #создаём пустое множество, в него будем заливать тикеры
            s = "https://iss.moex.com/iss/engines/stock/markets/shares/boards/TQBR/securities.xml?iss.meta=off"
            r = requests.get(s)
            root = xml.etree.ElementTree.fromstring(r.content)
            for data in root.findall("data"):
                if data.get("id") == "securities":
                    rows = data.find("rows")
                    for row in rows.findall("row"):
                        set_tickers.add(row.get("SECID")) #заливаем тикеры в наше множество
            async with aiofiles.open(r"D:\MoexAPI_bot_aiogram3\data_files\set_tickers.bin", "wb") as set_tickers_file_opened: #открываем файл для бинарной записи множества тикеров в него
                await set_tickers_file_opened.write(pickle.dumps(set_tickers)) #закидываем созданное множество в файл. Если что, каждый раз будет перезаписываться (проверено)
            #поменяем время последнего обновления
            info["last_day_check"]["ticker"] = str(datetime.datetime.now())
            async with aiofiles.open(r"D:\MoexAPI_bot_aiogram3\data_files\Info.json", "w", encoding="utf-8") as info_opened_file:
                await info_opened_file.write(json.dumps(info, indent = 3, ensure_ascii = False)) #запишем новый файл
        #теперь просто проверим есть ли тикер в списке тикеров
        async with aiofiles.open(r"D:\MoexAPI_bot_aiogram3\data_files\set_tickers.bin", "rb") as set_tickers_file_opened: #открываем файл с множеством тикеров чтобы его оттуда получить
            set_tickers = pickle.loads(await set_tickers_file_opened.read()) #из открытого файла выгружаем значение множества тикеров в переменную. Если вдруг запишется несколько множеств (такого быть не должно), то откроется только первое из них
        if self.name in set_tickers: #просто проверяем есть ли тикер в множестве тикеров
            return True
        else:
            return False
    async def CurrentPrice(self):
        """Текущая цена по этому тикеру"""
        s = "https://iss.moex.com/iss/engines/stock/markets/shares/boards/TQBR/securities/" + self.name + ".json?iss.meta=off"
        async with aiohttp.ClientSession() as session: #асинхронно получаем данные с сайта и заносим в словарь data
            async with session.get(s) as response:
                data = await response.json()
        return(data["marketdata"]["data"][0][12]) #находим нужный контейнер и смотрим внутри него на нужный атрибут, который и возвращаем как текущую цену тикера
    async def candles(self, candles_name: str, timeframe: str, start: str, end: str):
        """Лист свечей для этого тикера \n
        candles_name - необходимая составляющая свечей \n
        candles_name: open, close, high, low, value, volume, begin, end \n
        timeframe - таймфрейм: 1 - 1 мин, 10 - 10 мин, 60 - 1ч, 24 - 1д, 7 - 1н, 31 - 1мес, 4 - 4мес \n
        start, end - начало и конец периода, формат ГГГГ-ММ-ДД ЧЧ:ММ:СС
        """
        s = "https://iss.moex.com/iss/engines/stock/markets/shares/boards/TQBR/securities/" + self.name + f"/candles.xml?iss.meta=off&interval={timeframe}&till={end}&from={start}"
        r = requests.get(s)
        root = xml.etree.ElementTree.fromstring(r.content)
        candles = root.find("data")
        rows = candles.find("rows")
        listcandles = []
        if candles_name == "begin" or candles_name == "end": #для этих будем брать значения из iss в формате datetime подключенного модуля (дата и время)
            for row in rows.findall("row"):
                datetime_str = row.get(candles_name) #datetime_name сейчас строка в формате api
                #можно было бы datetime.datetime.strptime(), но там с годами не круто, не универсально. Поэтому так
                datetime_datetime = datetime.datetime(int(datetime_str[0:4]), int(datetime_str[5:7]), int(datetime_str[8:10]), int(datetime_str[11:13]), int(datetime_str[14:16]), int(datetime_str[17:])) #нарезаем строку с датой и временем на части даты и части времени,необходимые модулю datetime (год, месяц, день, час, минута, секунда). При этом не забывает всё сделать int
                listcandles.append(datetime_datetime)
        else:
            for row in rows.findall("row"):
                listcandles.append(float(row.get(candles_name)))#ВАЖЕН FLOAT, тк иначе импортируется строка,
                #а график строит строки тупо подряд, без адекватного выстроения значений по их значениям
        return(listcandles)
    def setattr_candles_dataframe(self, timeframe = str("24"), start = str(""), end = str("")):
        #создание датафрейма свечей как атрибута как минимум позволяет не передавать его каждый раз аргументом функции, накладывающей инструмент теханализа (тк она передаётся в self)
        """Создаёт датафрйм свечей с соответствующим timeframe, start и end и помещает в self.candles_dataframe \n
        Не при инициации, так как если тикер инициируется для получения текущей цены, нет причин делать лишние операции"""
                #создаём датафрейм всей инфы по свечам и заливаем её с помощью ранее написанного метода получения инфы по свечам
        candles_dataframe = pd.DataFrame({"open" :  self.candles("open", timeframe, start, end),
                                          "close" : self.candles("close", timeframe, start, end),
                                          "high" : self.candles("high", timeframe, start, end),
                                          "low" : self.candles("low", timeframe, start, end),
                                          "value" : self.candles("value", timeframe, start, end),
                                          "begin" : self.candles("begin", timeframe, start, end)
                                          #"end" вроде не нужно, бегина хватает
                                        })
        setattr(self, "candles_dataframe", candles_dataframe)
    def graphic(self, timeframe = str("24"), start = str(""), end = str("")):
        """возвращает открытый свечной график цены от времени \n
        timeframe - таймфрейм: 1 - 1 мин, 10 - 10 мин, 60 - 1ч, 24 - 1д, 7 - 1н, 31 - 1мес, 4 - 4мес | None = 24 \n
        start, end - начало и конец периода, формат ГГГГ-ММ-ДД ЧЧ:ММ:СС  | None = "" \n
        sma - нужная ли sma, sma_periods - массив периодов sma  | None = False, [] \n
        ema - нужная ли ema, ema_periods - массив периодов ema  | None = False, []\n
        """
        #создадим нужный датафрейм
        self.setattr_candles_dataframe(timeframe, start, end)
        #делаем up и down - новые датафреймы, части старого, но удовлетворяющие определённым условиям
        up = self.candles_dataframe[self.candles_dataframe.close >= self.candles_dataframe.open]
        down = self.candles_dataframe[self.candles_dataframe.close < self.candles_dataframe.open]
        #запишем это как атрибуты, так как некоторым инструментам теханализа важно, какие свечи растут, а какие падают
        setattr(self, "up", up)
        setattr(self, "down", down)
        #создадим width_big и width_small - ширины свечей, зависящие от таймфрейма
        #судя по всему 1 день по оси x соответствует 1 единице толщины столбика на диаграмме (питон вероятно умный)
        #хотя на 4мес уже не работает, хотя странно, потому что для всех остальных работает
        #но во всяком случае от увеличения или уменьшения диапазона свечи не начинают наезжать/иметь большие промежутки. Значит ширина связана именно с датами
        if timeframe == "1": #минута
            width_big = 1/24/60
        elif timeframe == "10": #10 минут
            width_big = 1/24/6
        elif timeframe == "60": #час
            width_big = 1/24
        elif timeframe == "24": #день
            width_big = 1
        elif timeframe == "7": #неделя
            width_big = 7
        elif timeframe == "31": #месяц
            width_big = 30
        elif timeframe == "4": #4 месяца
            width_big = 90
        else:
            width_big = 0 #такое по идее не может произойти
        width_small = width_big/10
        setattr(self, "width_big", width_big) #засунем width_big в self, чтобы потом использовать в инструментах теханализа, изображающихся как bar graph
        #разберёмся с теханализом. Для начала поймём сколько доп графиков для них нужно
        number_of_additional_graphics = int(0)
        for tech in self.tech_dict:
            if self.tech_dict[tech]["use"] and self.tech_dict[tech]["need_graph_space"]: #если инструмент теханализа используется И если этому инструменту теханала нужно место под доп график, посчитаем его
                number_of_additional_graphics += 1
        #если 1 и более инструментов теханала хотят доп график
        if number_of_additional_graphics != 0:
            height_rations_list = [10 - number_of_additional_graphics] + [1] * number_of_additional_graphics #массив отношений высот графиков, зависящий от числа графиков. Потом передадим его в subplots. Имеет вид [8, 1, 1]
            fig, axs = plt.subplots(nrows = 1 + number_of_additional_graphics, ncols = 1, sharex = True, height_ratios = height_rations_list) #создаём subplots. fig - контейнер графиков, axs[i] - iй график
            plt.suptitle(self.name, fontsize = 15) #заголовок - имя тикера
            axs[0].grid(True) #сетка для упрощения восприятия графика
            #заполняем его свечами up
            #это столбчатая диаграмма; plt.bar(x = ось x, height = высота столбика, width = ширина столбика, bottom = нижняя координата столбика, хз дальше странная * и потом ещё что-то непонятное)
            #ещё есть аргумент color, но в официальной документации я не нашёл. Возможно это входит в странную *
            axs[0].bar(x = up.begin, height = up.close - up.open,  width = width_big, bottom = up.open, color = "green") #для уточнения какой именно аргумент функции пишем можно писать имя_аргумента = значение_которое_даём
            axs[0].bar(x = up.begin, height = up.high - up.close,  width = width_small, bottom = up.close, color = "green")
            axs[0].bar(x = up.begin, height = up.open - up.low,  width = width_small, bottom = up.low, color = "green")
            #заполняем свечами down
            axs[0].bar(x = down.begin, height = down.open - down.close,  width = width_big, bottom = down.close, color = "red")
            axs[0].bar(x = down.begin, height = down.high - down.open,  width = width_small, bottom = down.open, color = "red")
            axs[0].bar(x = down.begin, height = down.close - down.low,  width = width_small, bottom = down.low, color = "red")
            #добавляем на график инструменты теханализа
            for tech in self.tech_dict:
                if self.tech_dict[tech]["use"]: #если инструмент теханализа используется
                    if self.tech_dict[tech]["use"] and not self.tech_dict[tech]["need_graph_space"]: #если не требует доп графика, вызовем соответствующую функцию
                        tech_func = getattr(self, tech) #теперь tech_func - это фукнция того теханализа, имя которого сейчас несёт в себе tech
                        tech_func(axs[0])
                    else : #если требует доп график, то
                        for i in range(number_of_additional_graphics):
                            tech_func = getattr(self, tech) #теперь уже tech - название функции, которая требует доп график
                            axs[i + 1].grid(True) #включим сетку также на каждом доп графике
                            tech_func(axs[i + 1]) #для каждого нового инструмента используем новый график
        #если 0 инструментов теханала просят доп график
        else:
            fig = plt.figure() #создаём контейнер графиков
            plt.title(self.name, fontsize = 15) #заголовок - имя тикера
            ax = fig.add_subplot() #ax - это сам график
            ax.grid(True) #сетка для упрощения восприятия графика
            #заполняем его свечами up
            #это столбчатая диаграмма; plt.bar(x = ось x, height = высота столбика, width = ширина столбика, bottom = нижняя координата столбика, хз дальше странная * и потом ещё что-то непонятное)
            #ещё есть аргумент color, но в официальной документации я не нашёл. Возможно это входит в странную *
            ax.bar(x = up.begin, height = up.close - up.open,  width = width_big, bottom = up.open, color = "green") #для уточнения какой именно аргумент функции пишем можно писать имя_аргумента = значение_которое_даём
            ax.bar(x = up.begin, height = up.high - up.close,  width = width_small, bottom = up.close, color = "green")
            ax.bar(x = up.begin, height = up.open - up.low,  width = width_small, bottom = up.low, color = "green")
            #заполняем свечами down
            ax.bar(x = down.begin, height = down.open - down.close,  width = width_big, bottom = down.close, color = "red")
            ax.bar(x = down.begin, height = down.high - down.open,  width = width_small, bottom = down.open, color = "red")
            ax.bar(x = down.begin, height = down.close - down.low,  width = width_small, bottom = down.low, color = "red")
            #добавляем на график инструменты теханализа, не требующие доп графика (в данном разделе это все используемые инструменты, так как раньше было условие о том, что нет инструментов с доп графиком)
            for tech in self.tech_dict:
                if self.tech_dict[tech]["use"]: #если используется и не требует доп графика, вызовем соответствующую функцию
                    tech_func = getattr(self, tech) #теперь tech_func - это фукнция того теханализа, имя которого сейчас несёт в себе tech, при этом подвязанная к self. Иначе говоря её применение аналогично применению self.sma(...) при tech = sma
                    tech_func(ax)

        #сохраняем график как картинку и ретёрним её открытую для отправки
        fig.savefig(r"D:\Python files\!MoexApiBot\graphic.png")
        opened_graphic = open(r"D:\Python files\!MoexApiBot\graphic.png", "rb")
        return opened_graphic
    def sma(self, ax):
        for period in self.tech_dict["sma"]["periods"]: #для каждого нужного периода sma создадим список значений sma и докинем его в график
            if period <=  len(self.candles_dataframe.begin): #так как иначе при построении графика список оси x пуст, а оси y не пуст (потому что там есть база рекурренты)
                sma_list = [] #список значений sma (соответсует датам из датафрейма)
                sma_list.append(sum(self.candles_dataframe.close[0: period])/period) #делаем рекуррентой, чтобы не считать каждый раз большую сумму
                for i in range(period, len(self.candles_dataframe.begin)): #начало сдвинуто, тк sma считается не раньше чем из period свечей
                    sma_list.append(sma_list[i - period] + (self.candles_dataframe.close[i] - self.candles_dataframe.close[i - period])/period) #добавим новую свечу к прошлому значению sma и уберём самую старую
                ax.plot(self.candles_dataframe.begin[period - 1:], sma_list) #тут нужен срез по оси x, чтобы осциллятор начинался с даты, с которой мы его считаем
    def ema(self, ax):
        for period in self.tech_dict["ema"]["periods"]:
            if period <=  len(self.candles_dataframe.begin): #так как иначе при построении графика список оси x пуст, а оси y не пуст (потому что там есть база рекурренты)
                ema_list = []
                ema_list.append(sum(self.candles_dataframe.close[0: period])/period) #первое значение ema - это sma по тому же периоду
                for i in range(period, len(self.candles_dataframe.begin)):
                    ema_list.append(((period - 1)*ema_list[i - period] + 2 * self.candles_dataframe.close[i])/(period + 1))
                ax.plot(self.candles_dataframe.begin[period - 1:], ema_list)
    def value(self, ax):
        ax.bar(x = self.up.begin, height = self.up.value, width = self.width_big, color = "green")
        ax.bar(x = self.down.begin, height = self.down.value, width = self.width_big, color = "red")
        ax.set_title("Value", fontsize = 7)

"""
Тесты
"""

"""
beb = ticker("SBER")
beb.setattr_candles_dataframe("24", "2024-01-01", "2024-01-07")
print(beb.candles_dataframe)
"""

"""
beb.tech_dict["value"]["use"] = True
beb.graphic("24", "2024-01-01", "2024-10-01")
plt.show
"""

"""
beb = ticker("SBER")
beb.tech_dict["sma"]["use"] = True
beb.tech_dict["sma"]["periods"] = [20, 10]
beb.tech_dict["ema"]["use"] = True
beb.tech_dict["ema"]["periods"] = [150, 250]
beb.tech_dict["value"]["use"] = True
beb.graphic("24", "2024-01-01", "2024-05-01")
"""

Instruction: перепиши асинхронно с aiohttp

import pytest
import inspect
import ast
import asyncio
import re
import sys
import os
import io
import xml.etree.ElementTree as ET
import textwrap
from unittest.mock import patch, AsyncMock, MagicMock, Mock, mock_open
import importlib.util

# Add pytest-asyncio marker to tell pytest to handle coroutines properly
pytest_plugins = ["pytest_asyncio"]

@pytest.fixture
def mock_aiohttp_response():
    """Create a mock aiohttp response for API calls."""
    mock_resp = AsyncMock()
    # Return properly formatted XML for candles tests
    mock_resp.read = AsyncMock(return_value=b'''
    <document>
      <data id="candles">
        <rows>
          <row open="100.0" close="110.0" high="115.0" low="95.0" value="1000.0" volume="500" begin="2023-01-01 00:00:00" end="2023-01-01 23:59:59" />
          <row open="110.0" close="120.0" high="125.0" low="105.0" value="1200.0" volume="600" begin="2023-01-02 00:00:00" end="2023-01-02 23:59:59" />
        </rows>
      </data>
    </document>
    ''')
    # Set up JSON response for CurrentPrice tests
    mock_resp.json = AsyncMock(return_value={
        "marketdata": {
            "data": [[None, None, None, None, None, None, None, None, None, None, None, None, 150.5]]
        }
    })
    mock_resp.text = AsyncMock(return_value="Success")
    mock_resp.status = 200
    return mock_resp

@pytest.fixture
def mock_requests_response():
    """Create a mock for requests operations."""
    mock_resp = MagicMock()
    mock_resp.content = b'''
    <document>
      <data id="candles">
        <rows>
          <row open="100.0" close="110.0" high="115.0" low="95.0" value="1000.0" volume="500" begin="2023-01-01 00:00:00" end="2023-01-01 23:59:59" />
          <row open="110.0" close="120.0" high="125.0" low="105.0" value="1200.0" volume="600" begin="2023-01-02 00:00:00" end="2023-01-02 23:59:59" />
        </rows>
      </data>
    </document>
    '''
    return mock_resp

@pytest.fixture
def mock_aiofiles():
    """Create a mock for aiofiles operations."""
    # Create more robust file mock that works with open and read
    mock_file = AsyncMock()
    mock_file.read.return_value = '{"last_day_check": {"ticker": "2020-01-01 00:00:00.000000"}}'
    mock_file.write.return_value = None

    mock_context = AsyncMock()
    mock_context.__aenter__.return_value = mock_file

    with patch('aiofiles.open', return_value=mock_context):
        yield mock_file

@pytest.fixture
def mock_pickle():
    """Create a mock for pickle operations."""
    with patch('pickle.dumps', return_value=b'mock_pickle_data') as dumps_mock, \
         patch('pickle.loads', return_value={'SBER', 'GAZP'}) as loads_mock:
        yield loads_mock

def find_ticker_class(module):
    """Find the ticker class in a module, regardless of naming convention."""
    # Try common names first
    possible_names = ['ticker', 'Ticker', 'TICKER']
    for name in possible_names:
        if hasattr(module, name):
            return getattr(module, name)

    # Look for any class that might be a ticker class
    for attr_name in dir(module):
        attr = getattr(module, attr_name)
        if isinstance(attr, type):
            # Check if this class has methods that a ticker class would have
            if (hasattr(attr, 'correct_name') or
                hasattr(attr, 'CurrentPrice') or
                hasattr(attr, 'candles')):
                return attr

    return None

@pytest.fixture
def ticker_class(implementation):
    """Get the ticker class from the implementation."""
    impl_name, module = implementation

    ticker_cls = find_ticker_class(module)
    if ticker_cls is None:
        pytest.skip(f"Implementation {impl_name} does not have a recognizable ticker class")

    return ticker_cls

@pytest.fixture
def async_ticker_instance(ticker_class):
    """Get a ticker instance from the implementation for async tests."""
    ticker_instance = ticker_class('SBER')
    return ticker_instance

def test_ticker_class_exists(implementation):
    """Test that the ticker class exists in the implementation."""
    impl_name, module = implementation

    ticker_cls = find_ticker_class(module)
    if ticker_cls is None:
        # Try to find any class definitions
        all_objects = dir(module)
        classes = [obj for obj in all_objects if isinstance(getattr(module, obj), type)]
        if classes:
            pytest.skip(f"Implementation {impl_name} has classes {classes} but no suitable ticker class found")
        else:
            pytest.fail(f"Implementation {impl_name} should have a ticker class")

def test_required_methods_exist(ticker_class):
    """Test that the required methods exist in the ticker class."""
    required_methods = ['correct_name']

    for method_name in required_methods:
        assert hasattr(ticker_class, method_name), \
            f"Ticker class should have a {method_name} method"

def test_all_async_methods_properly_handled(ticker_class):
    """Test that all methods that should be async are properly marked as async."""
    # Methods that should be async according to the instruction
    required_async_methods = ['correct_name']

    for method_name in required_async_methods:
        if not hasattr(ticker_class, method_name):
            pytest.skip(f"Ticker class does not have a {method_name} method")

        method = getattr(ticker_class, method_name)
        assert asyncio.iscoroutinefunction(method), \
            f"Method {method_name} should be async"

def test_import_structure(implementation):
    """Test that the required imports are present."""
    impl_name, module = implementation

    # Get the source code
    module_path = module.__file__
    try:
        with open(module_path, 'r', encoding='utf-8') as f:
            source = f.read()

        # Parse the AST
        tree = ast.parse(source)

        # Extract imports
        imports = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for name in node.names:
                    imports.append(name.name)
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    # Including base module in imports list
                    imports.append(node.module)
                for name in node.names:
                    if node.module:
                        imports.append(f"{node.module}.{name.name}")
                    else:
                        imports.append(name.name)

        # Check that aiohttp and aiofiles are imported
        assert any('aiohttp' in imp for imp in imports), \
            f"Implementation {impl_name} should import aiohttp"
        assert any('aiofiles' in imp for imp in imports), \
            f"Implementation {impl_name} should import aiofiles"
    except Exception as e:
        pytest.skip(f"Implementation {impl_name} has issues: {str(e)}")

def test_no_sync_http_calls_in_async_methods(ticker_class):
    """Test that async methods don't use synchronous HTTP calls."""
    # Methods that should be async and not use sync HTTP calls
    async_methods = ['correct_name']

    for method_name in async_methods:
        if not hasattr(ticker_class, method_name):
            pytest.skip(f"Ticker class does not have a {method_name} method")

        method = getattr(ticker_class, method_name)
        if asyncio.iscoroutinefunction(method):
            try:
                # Get the source code and fix indentation
                source = inspect.getsource(method)
                source = textwrap.dedent(source)

                # Check for requests.get direct usage with simple string matching first
                if "requests.get" in source:
                    pytest.fail(f"Method {method_name} appears to use synchronous requests.get")

                # Try to parse the AST for more detailed analysis
                try:
                    tree = ast.parse(source)

                    sync_calls_found = False
                    for node in ast.walk(tree):
                        if isinstance(node, ast.Call):
                            if isinstance(node.func, ast.Attribute):
                                # Check for requests.get pattern
                                if getattr(node.func, 'attr', '') == 'get' and \
                                   isinstance(node.func.value, ast.Name) and \
                                   getattr(node.func.value, 'id', '') == 'requests':
                                    sync_calls_found = True
                                    break

                    if sync_calls_found:
                        pytest.fail(f"Async method {method_name} should not use synchronous requests.get")

                except SyntaxError:
                    # If we can't parse the AST, fall back to the string check we did earlier
                    pass

            except (OSError, IOError, TypeError) as e:
                pytest.skip(f"Could not analyze source code for {method_name}: {str(e)}")

@pytest.mark.asyncio
async def test_async_correct_name_method(async_ticker_instance, mock_aiohttp_response, mock_aiofiles, mock_pickle):
    """Test the correct_name method with mocked aiohttp."""

    # Create a proper awaitable ClientSession mock
    session_mock = AsyncMock()
    get_mock = AsyncMock()
    get_context_mock = AsyncMock()
    get_context_mock.__aenter__.return_value = mock_aiohttp_response
    get_mock.return_value = get_context_mock
    session_mock.get = get_mock

    session_context_mock = AsyncMock()
    session_context_mock.__aenter__.return_value = session_mock

    # Patch aiohttp ClientSession to return our configured mock
    with patch('aiohttp.ClientSession', return_value=session_context_mock):
        try:
            # Patch open to avoid file system access
            with patch('aiofiles.open', return_value=mock_aiofiles):
                # Properly set up the mock pickle data
                with patch('pickle.loads', return_value={'SBER', 'GAZP'}):
                    try:
                        # Execute the method with timeout to prevent hanging
                        result = await asyncio.wait_for(async_ticker_instance.correct_name(), timeout=5.0)

                        # Check that result is as expected
                        assert result is True, "correct_name should return True for SBER"
                    except asyncio.TimeoutError:
                        pytest.skip("The correct_name method timed out")

        except Exception as e:
            pytest.skip(f"Method has implementation issues: {str(e)}")

pytest
pytest-mock
pytest-asyncio
aiohttp
aiofiles
matplotlib
pandas
requests

import pytest
import os
import sys
import json
from typing import Dict, List, Optional, Any

# Import from local test_utils.py in the same directory
from test_utils import TestUtils, TestResultsManager

# Load all implementations in the current sandbox
implementations = TestUtils.load_all_implementations()
test_results = TestResultsManager()

@pytest.fixture(scope="session")
def sandbox_dir():
    """Fixture to provide the sandbox directory path."""
    return os.path.dirname(os.path.abspath(__file__))

@pytest.fixture(scope="session")
def sandbox_name():
    """Fixture to provide the sandbox name."""
    return os.path.basename(os.path.dirname(os.path.abspath(__file__)))

@pytest.fixture(scope="session")
def all_implementations():
    """Fixture to provide all implementations as a dictionary."""
    return implementations

@pytest.fixture(params=list(implementations.items()))
def implementation(request):
    """Fixture to provide each implementation to tests one at a time."""
    return request.param

@pytest.fixture(scope="session")
def results_manager():
    """Fixture to provide access to the test results manager."""
    return test_results

# Hook for collecting test results
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """Pytest hook to collect test results."""
    # Execute all other hooks to obtain the report object
    outcome = yield
    rep = outcome.get_result()

    # We're only interested in the call outcome
    if rep.when == "call":
        if hasattr(item, "callspec") and "implementation" in item.callspec.params:
            # Get implementation name and module
            impl_name, _ = item.callspec.params["implementation"]

            # Get test name
            test_name = item.nodeid.split("::")[-1]

            # Record result
            if rep.passed:
                test_results.record_result(impl_name, test_name, True)
            elif rep.failed:
                error_msg = str(rep.longrepr) if rep.longrepr else "Test failed"
                test_results.record_result(impl_name, test_name, False, error_msg)
            elif rep.skipped:
                skip_reason = rep.longrepr[2] if rep.longrepr else "Test skipped"
                test_results.record_skip(impl_name, test_name, skip_reason)

# Hook to save results at the end of testing
@pytest.hookimpl(trylast=True)
def pytest_sessionfinish(session, exitstatus):
    """Save test results at the end of the test session."""
    test_results.save_results()

import os
import sys
import glob
import re
import importlib.util
import traceback
import types
from typing import Dict, List, Optional, Any, Tuple

class TestUtils:
    @staticmethod
    def discover_implementation_files(directory: str = None) -> List[str]:
        """Find all implementation files in the current sandbox directory."""
        if directory is None:
            directory = os.path.dirname(os.path.abspath(__file__))

        patterns = [
            r'modified_code\d+\.py',
            r'new_code\d+\.py',
            r'original_code\.py',
            r'implementation\d*\.py'
        ]

        pattern = re.compile('|'.join(f'({p})' for p in patterns))
        implementations = []

        for file_path in glob.glob(os.path.join(directory, '*.py')):
            if pattern.search(os.path.basename(file_path)):
                implementations.append(file_path)

        # Sort files numerically
        def sort_key(path):
            filename = os.path.basename(path)
            match = re.search(r'(\d+)', filename)
            return int(match.group(1)) if match else 0

        return sorted(implementations, key=sort_key)

    @staticmethod
    def create_mock_module(file_path: str, module_name: str, error_info: str) -> types.ModuleType:
        """Create a mock module that contains error information but can still be tested."""
        # Create a new module object
        mock_module = types.ModuleType(module_name)

        # Add basic attributes
        mock_module.__file__ = file_path
        mock_module.__name__ = module_name
        mock_module.__display_name__ = module_name
        mock_module.__error__ = error_info

        # Add a dummy function that can be detected by test functions
        def dummy_function(*args, **kwargs):
            return f"Error in module: {error_info}"

        setattr(mock_module, "implementation_error", dummy_function)

        return mock_module

    @staticmethod
    def load_module(file_path: str, module_name: Optional[str] = None) -> Any:
        """
        Safely load a module from a file path with proper error handling.
        If the module has errors, return a mock module that can still be tested.
        """
        if module_name is None:
            module_name = os.path.basename(file_path).replace('.py', '')

        # Create a unique module name to avoid conflicts
        sandbox_id = os.path.basename(os.path.dirname(file_path))
        unique_module_name = f"{sandbox_id}_{module_name}"

        try:
            # First, try to read the file to check for syntax errors
            with open(file_path, 'r') as f:
                source_code = f.read()

            # Check for syntax errors by compiling the code
            try:
                compiled = compile(source_code, file_path, 'exec')
            except SyntaxError as e:
                error_msg = f"Syntax error: {str(e)}"
                print(f"Syntax error in {file_path}: {e}")
                print(f"  Line {e.lineno}, column {e.offset}: {e.text}")
                return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

            # Create the module spec
            spec = importlib.util.spec_from_file_location(unique_module_name, file_path)
            if spec is None or spec.loader is None:
                error_msg = f"Could not create spec for {file_path}"
                print(f"Error: {error_msg}")
                return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

            # Create the module object
            module = importlib.util.module_from_spec(spec)
            sys.modules[unique_module_name] = module

            # Special handling for execution errors
            try:
                # Execute the module code in a safe way
                spec.loader.exec_module(module)
                # Store the original name for reference
                module.__display_name__ = module_name
                return module
            except Exception as e:
                error_msg = f"Runtime error: {str(e)}"
                traceback_str = traceback.format_exc()
                print(f"Error executing module {file_path}: {e}")
                print(traceback_str)

                # Create a partial module that contains what we loaded before the error
                mock_module = TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

                # Copy any attributes that might have been defined before the error
                for attr_name in dir(module):
                    if not attr_name.startswith('__'):
                        try:
                            setattr(mock_module, attr_name, getattr(module, attr_name))
                        except Exception:
                            pass  # Skip attributes that can't be copied

                return mock_module

        except FileNotFoundError as e:
            error_msg = f"File not found: {str(e)}"
            print(f"Error: {error_msg}")
            return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)
        except Exception as e:
            error_msg = f"Unexpected error: {str(e)}"
            print(f"Error loading module {file_path}: {e}")
            return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

    @classmethod
    def load_all_implementations(cls, directory: str = None) -> Dict[str, Any]:
        """Load all implementation files in the directory, including those with errors."""
        if directory is None:
            directory = os.path.dirname(os.path.abspath(__file__))

        implementations = {}

        implementation_files = cls.discover_implementation_files(directory)
        if not implementation_files:
            print("WARNING: No implementation files found. Check your file naming patterns.")

        for file_path in implementation_files:
            module_name = os.path.basename(file_path).replace('.py', '')
            module = cls.load_module(file_path, module_name)

            # Always add the module, even if it has errors
            implementations[module_name] = module

            if hasattr(module, '__error__'):
                print(f"Loaded with errors: {module_name} - {module.__error__}")
            else:
                print(f"Successfully loaded: {module_name}")

        return implementations

class TestResultsManager:
    def __init__(self):
        self.results = {}
        self.sandbox_name = os.path.basename(os.path.dirname(os.path.abspath(__file__)))

    def record_result(self, impl_name: str, test_name: str, passed: bool,
                     error_msg: Optional[str] = None) -> None:
        """Record a test result for an implementation."""
        if impl_name not in self.results:
            self.results[impl_name] = {"passed": 0, "failed": 0, "skipped": 0, "errors": []}

        if passed:
            self.results[impl_name]["passed"] += 1
        else:
            self.results[impl_name]["failed"] += 1
            if error_msg:
                self.results[impl_name]["errors"].append({
                    "test": test_name,
                    "error": error_msg
                })

    def record_skip(self, impl_name: str, test_name: str,
                   reason: Optional[str] = None) -> None:
        """Record a skipped test for an implementation."""
        if impl_name not in self.results:
            self.results[impl_name] = {"passed": 0, "failed": 0, "skipped": 0, "errors": []}

        self.results[impl_name]["skipped"] += 1
        if reason:
            self.results[impl_name]["errors"].append({
                "test": test_name,
                "error": f"SKIPPED: {reason}"
            })

    def get_winner(self) -> Tuple[Optional[int], Dict]:
        """Determine the winner based on test results."""
        winner = None
        max_passed = -1

        for impl_name, results in self.results.items():
            if impl_name == "original_code":
                continue  # Skip original code when determining winner

            if results["passed"] > max_passed:
                max_passed = results["passed"]
                winner = impl_name
            # Break ties by looking at failure count
            elif results["passed"] == max_passed and winner is not None:
                if results["failed"] < self.results[winner]["failed"]:
                    winner = impl_name

        # Convert winner to numeric index if possible
        winner_index = -1
        if winner and re.match(r'modified_code\d+', winner):
            try:
                winner_index = int(re.search(r'(\d+)', winner).group(1))
            except (AttributeError, ValueError):
                pass

        return winner_index, self.results

    def save_results(self, filename: str = "test_results.json") -> None:
        """Save test results to a JSON file."""
        import json

        winner_index, results = self.get_winner()

        # Check if all tests were skipped
        all_skipped = all(
            stats["skipped"] == stats["passed"] + stats["failed"] + stats["skipped"]
            for impl_name, stats in results.items()
            if impl_name != "original_code"
        )

        output = {
            "winner": winner_index,
            "all_skipped": all_skipped,
            "results": {
                name: {
                    "passed": stats["passed"],
                    "failed": stats["failed"],
                    "skipped": stats["skipped"],
                    "total": stats["passed"] + stats["failed"] + stats["skipped"]
                }
                for name, stats in results.items()
                if not name.startswith("_")  # Skip internal items
            }
        }

        with open(filename, "w") as f:
            json.dump(output, f, indent=2)

        print(f"Test results saved to {filename}")

        return output


class Table:
    def __init__(self, data, types, copy_table=False):
        assert set(map(type, data)) == {list}
        assert len(set(data[0])) == len(data[0])
        assert len(set(map(len, data))) == 1
        assert len(data[0]) == len(types)
        self.data = [line[:] for line in data] if copy_table else data
        assert set(types.keys()) == set(self.data[0])
        self.types = types

    def print_table(self):
        types = self.get_column_types()
        print(self.data[0])
        for row in self.data[1:]:
            print([types[i](val) for i, val in enumerate(row)])
        print([self.types[i] for i in self.data[0]])

    def get_rows_by_number(self, start, stop=None, copy_table=False):
        assert start > 0
        if stop is None:
            stop = start+1
        else:
            assert stop > start
            stop += 1
        return Table(self.data[:1] + self.data[start:stop], self.types)

    def get_rows_by_index(self, *vals, copy_table=False):
        ids = self.get_values()
        rows = [self.data[ids.index(val)+1] for val in vals]
        return Table(self.data[:1] + rows, self.types, copy_table)

    def get_column_types(self, by_number=True):
        if by_number:
            return {i:self.types[val] for i, val in enumerate(self.data[0])}
        else:
            return self.types

    def set_column_types(self, types, by_number=True):
        if by_number:
            self.types = {self.data[0][i]: val for i, val in types.items()}
        else:
            self.types = types

    def get_values(self, column=0):
        if not isinstance(column, int):
            column = self.data[0].index(column)
        return [self.get_column_types()[column](row[column]) for row in self.data[1:]]

    def get_value(self, column=0):
        assert len(self.data) == 2
        if not isinstance(column, int):
            column = self.data[0].index(column)
        return self.get_column_types()[column](self.data[1][column])

    def set_values(self, values, column=0):
        if not isinstance(column, int):
            column = self.data[0].index(column)
        for i, value in enumerate(values):
            self.data[i + 1][column] = value

    def set_value(self, value, column=0):
        assert len(self.data) == 2
        if not isinstance(column, int):
            column = self.data[0].index(column)
        self.data[1][column] = value

    def concat(self, table):
        assert self.data[0] == table.data[0]
        assert self.types == table.types
        data = self.data + table.data[1:]
        return Table(data, self.types)

    def split(self, row_number):
        return Table(self.data[:row_number], self.types), Table(self.data[:1] + self.data[row_number:], self.types)

    def add(self, col1, col2):
        val1, val2 = self.get_values(col1), self.get_values(col2)
        return [v1 + v2 for v1, v2 in zip(val1, val2)]

    def sub(self, col1, col2):
        val1, val2 = self.get_values(col1), self.get_values(col2)
        return [v1 - v2 for v1, v2 in zip(val1, val2)]

    def mul(self, col1, col2):
        val1, val2 = self.get_values(col1), self.get_values(col2)
        return [v1 * v2 for v1, v2 in zip(val1, val2)]

    def div(self, col1, col2):
        val1, val2 = self.get_values(col1), self.get_values(col2)
        return [v1 / v2 for v1, v2 in zip(val1, val2)]

    def merge_tables(self, table, by_number=True):
        data = [row+table[i if by_number else table.get_values().index(row[0])+1] for i, row in enumerate(self.data)]
        return Table(data, {**self.types, **table.types})

Instruction: функция save_table, сохраняющая в текстовом файле представление таблицы, аналогичное выводу на печать с помощью функции print_table()

import io
import os
import tempfile
import pytest
from contextlib import redirect_stdout
import inspect
import copy

def test_save_table_method_exists(implementation):
    """Test that the save_table method exists in the implementation."""
    impl_name, module = implementation

    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

def test_save_table_method_signature(implementation):
    """Test that the save_table method has the correct signature."""
    impl_name, module = implementation

    # Verify Table class and save_table method
    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

    # Check if save_table requires a filename parameter
    sig = inspect.signature(module.Table.save_table)
    params = list(sig.parameters.keys())
    assert len(params) >= 2, f"{impl_name}'s save_table method should have at least 2 parameters (self, filename)"
    assert params[1] == 'filename', f"{impl_name}'s save_table method should have 'filename' as its second parameter"

def test_save_table_writes_to_file(implementation):
    """Test that save_table writes to a file."""
    impl_name, module = implementation

    # Verify Table class and save_table method
    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

    # Create a simple table for testing
    data = [['col1', 'col2'], ['1', '2']]
    types = {'col1': int, 'col2': int}
    table = module.Table(data, types)

    # Create a temporary file and save table to it
    with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file:
        temp_filename = temp_file.name

        try:
            table.save_table(temp_filename)

            # Check if file exists and has content
            assert os.path.exists(temp_filename), f"{impl_name}'s save_table method didn't create a file"

            with open(temp_filename, 'r') as f:
                content = f.read()
                assert content.strip(), f"{impl_name}'s save_table method did not write anything to the file"
        finally:
            # Clean up
            if os.path.exists(temp_filename):
                os.unlink(temp_filename)

def test_save_table_output_matches_print_table(implementation):
    """Test that save_table output matches print_table output."""
    impl_name, module = implementation

    # Verify Table class and save_table method
    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

    # Create a test table
    data = [
        ['name', 'age', 'height'],
        ['Alice', '30', '165.5'],
        ['Bob', '25', '180.0']
    ]
    types = {'name': str, 'age': int, 'height': float}
    table = module.Table(data, types)

    # Capture print_table output
    captured_output = io.StringIO()
    with redirect_stdout(captured_output):
        table.print_table()
    print_output = captured_output.getvalue().strip()

    # Save table to temporary file
    with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file:
        temp_filename = temp_file.name

        try:
            table.save_table(temp_filename)

            # Read file content
            with open(temp_filename, 'r') as f:
                file_content = f.read().strip()

            # Compare content (normalizing whitespace)
            print_lines = [line.strip() for line in print_output.split('\n') if line.strip()]
            file_lines = [line.strip() for line in file_content.split('\n') if line.strip()]

            assert len(print_lines) == len(file_lines), (
                f"{impl_name}'s save_table output has {len(file_lines)} lines, "
                f"while print_table has {len(print_lines)} lines"
            )

            # Check each line (allowing for format variations)
            for i, (print_line, file_line) in enumerate(zip(print_lines, file_lines)):
                # Normalize lines by removing all whitespace and punctuation
                clean_print = ''.join(c for c in print_line if c.isalnum() or c == '.' or c == '-')
                clean_file = ''.join(c for c in file_line if c.isalnum() or c == '.' or c == '-')

                assert clean_print == clean_file, (
                    f"{impl_name}'s line {i+1} content differs between print_table and save_table:\n"
                    f"print: {print_line}\nfile: {file_line}"
                )
        finally:
            # Clean up
            if os.path.exists(temp_filename):
                os.unlink(temp_filename)

def test_save_table_with_complex_data(implementation):
    """Test save_table with a more complex dataset."""
    impl_name, module = implementation

    # Verify Table class and save_table method
    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

    # Test with a more complex dataset and types
    data = [
        ['id', 'name', 'score', 'active'],
        ['1', 'Alice', '95.5', 'True'],
        ['2', 'Bob', '87.3', 'False'],
        ['3', 'Charlie', '76.8', 'True']
    ]
    types = {'id': int, 'name': str, 'score': float, 'active': bool}
    table = module.Table(data, types)

    # Save the table
    with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file:
        temp_filename = temp_file.name

        try:
            table.save_table(temp_filename)

            # Check file exists and read content
            assert os.path.exists(temp_filename), f"{impl_name}'s save_table method didn't create a file"

            with open(temp_filename, 'r') as f:
                content = f.read()
                lines = content.strip().split('\n')

                # Basic structure checks
                assert len(lines) >= 5, f"{impl_name}'s save_table output has {len(lines)} lines, expected at least 5"

                # Check for expected data in the content (case-insensitive)
                full_content_lower = content.lower()
                expected_items = ['id', 'name', 'score', 'active', 'alice', 'bob', 'charlie']

                for item in expected_items:
                    assert item.lower() in full_content_lower, f"{impl_name}'s saved content is missing '{item}'"

                # Check for numeric values (ignoring decimal separator variations)
                expected_numbers = ['1', '2', '3', '95.5', '87.3', '76.8']
                for num in expected_numbers:
                    num_parts = num.split('.')
                    if len(num_parts) == 2:  # It's a float
                        # Check for both dot and comma as decimal separator
                        assert (num_parts[0] in full_content_lower and
                                (num_parts[1] in full_content_lower or
                                 num_parts[0] + ',' + num_parts[1] in full_content_lower)), \
                                f"{impl_name}'s saved content is missing number '{num}'"
                    else:  # It's an integer
                        assert num in full_content_lower, f"{impl_name}'s saved content is missing number '{num}'"

                # Check for type information
                type_indicators = ['int', 'str', 'float', 'bool']
                for type_name in type_indicators:
                    assert type_name.lower() in full_content_lower, \
                        f"{impl_name}'s saved content is missing type indicator '{type_name}'"
        finally:
            # Clean up
            if os.path.exists(temp_filename):
                os.unlink(temp_filename)

def test_save_table_does_not_modify_table(implementation):
    """Test that save_table does not modify the table data."""
    impl_name, module = implementation

    # Verify Table class and save_table method
    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

    # Create a test table
    data = [
        ['name', 'value'],
        ['item1', '10'],
        ['item2', '20']
    ]
    types = {'name': str, 'value': int}
    table = module.Table(data, types)

    # Create deep copies of data and types for comparison
    original_data = copy.deepcopy(table.data)
    original_types = copy.deepcopy(table.types)

    # Save the table to a temporary file
    with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file:
        temp_filename = temp_file.name

        try:
            table.save_table(temp_filename)

            # Check that table data and types were not modified
            assert table.data == original_data, f"{impl_name}'s save_table method modified the table data"
            assert table.types == original_types, f"{impl_name}'s save_table method modified the table types"
        finally:
            # Clean up
            if os.path.exists(temp_filename):
                os.unlink(temp_filename)

def test_save_table_respects_column_types(implementation):
    """Test that save_table respects column types when saving."""
    impl_name, module = implementation

    # Verify Table class and save_table method
    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

    # Create a test table with various data types
    data = [
        ['int_col', 'float_col', 'str_col', 'bool_col'],
        ['123', '45.67', 'hello', 'True'],
        ['456', '78.90', 'world', 'False']
    ]
    types = {'int_col': int, 'float_col': float, 'str_col': str, 'bool_col': bool}
    table = module.Table(data, types)

    # Save the table
    with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file:
        temp_filename = temp_file.name

        try:
            table.save_table(temp_filename)

            # Read the saved content
            with open(temp_filename, 'r') as f:
                content = f.read()

            content_lower = content.lower()

            # Verify integers are correctly represented
            assert '123' in content_lower, f"{impl_name}'s save_table output is missing integer value '123'"
            assert '456' in content_lower, f"{impl_name}'s save_table output is missing integer value '456'"

            # Verify floats (allowing for decimal separator variations)
            assert ('45.67' in content_lower or '45,67' in content_lower), \
                f"{impl_name}'s save_table output is missing float value '45.67'"
            assert ('78.90' in content_lower or '78,90' in content_lower), \
                f"{impl_name}'s save_table output is missing float value '78.90'"

            # Verify strings
            assert 'hello' in content_lower, f"{impl_name}'s save_table output is missing string value 'hello'"
            assert 'world' in content_lower, f"{impl_name}'s save_table output is missing string value 'world'"

            # Verify booleans
            assert ('true' in content_lower and 'false' in content_lower), \
                f"{impl_name}'s save_table output is missing boolean values 'True'/'False'"

            # Check for type information
            type_indicators = ['int', 'float', 'str', 'bool']
            for type_name in type_indicators:
                assert type_name.lower() in content_lower, \
                    f"{impl_name}'s save_table output is missing type indicator '{type_name}'"
        finally:
            # Clean up
            if os.path.exists(temp_filename):
                os.unlink(temp_filename)

def test_save_table_handles_empty_table(implementation):
    """Test that save_table can handle a table with only headers."""
    impl_name, module = implementation

    # Verify Table class and save_table method
    assert hasattr(module, 'Table'), f"{impl_name} does not have a Table class"
    assert hasattr(module.Table, 'save_table'), f"{impl_name} does not have a save_table method"

    # Create a table with only header row (no data rows)
    data = [['col1', 'col2', 'col3']]
    types = {'col1': int, 'col2': float, 'col3': str}
    table = module.Table(data, types)

    # Save the table
    with tempfile.NamedTemporaryFile(mode='w+', delete=False) as temp_file:
        temp_filename = temp_file.name

        try:
            table.save_table(temp_filename)

            # Verify file exists and contains headers
            with open(temp_filename, 'r') as f:
                content = f.read()

            # Check that the header and types are present
            content_lower = content.lower()
            assert 'col1' in content_lower, f"{impl_name}'s save_table output is missing header 'col1'"
            assert 'col2' in content_lower, f"{impl_name}'s save_table output is missing header 'col2'"
            assert 'col3' in content_lower, f"{impl_name}'s save_table output is missing header 'col3'"

            # Check for type information
            assert 'int' in content_lower, f"{impl_name}'s save_table output is missing type 'int'"
            assert 'float' in content_lower, f"{impl_name}'s save_table output is missing type 'float'"
            assert 'str' in content_lower, f"{impl_name}'s save_table output is missing type 'str'"
        finally:
            # Clean up
            if os.path.exists(temp_filename):
                os.unlink(temp_filename)

import pytest
import os
import sys
import json
from typing import Dict, List, Optional, Any

# Import from local test_utils.py in the same directory
from test_utils import TestUtils, TestResultsManager

# Load all implementations in the current sandbox
implementations = TestUtils.load_all_implementations()
test_results = TestResultsManager()

@pytest.fixture(scope="session")
def sandbox_dir():
    """Fixture to provide the sandbox directory path."""
    return os.path.dirname(os.path.abspath(__file__))

@pytest.fixture(scope="session")
def sandbox_name():
    """Fixture to provide the sandbox name."""
    return os.path.basename(os.path.dirname(os.path.abspath(__file__)))

@pytest.fixture(scope="session")
def all_implementations():
    """Fixture to provide all implementations as a dictionary."""
    return implementations

@pytest.fixture(params=list(implementations.items()))
def implementation(request):
    """Fixture to provide each implementation to tests one at a time."""
    return request.param

@pytest.fixture(scope="session")
def results_manager():
    """Fixture to provide access to the test results manager."""
    return test_results

# Hook for collecting test results
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """Pytest hook to collect test results."""
    # Execute all other hooks to obtain the report object
    outcome = yield
    rep = outcome.get_result()

    # We're only interested in the call outcome
    if rep.when == "call":
        if hasattr(item, "callspec") and "implementation" in item.callspec.params:
            # Get implementation name and module
            impl_name, _ = item.callspec.params["implementation"]

            # Get test name
            test_name = item.nodeid.split("::")[-1]

            # Record result
            if rep.passed:
                test_results.record_result(impl_name, test_name, True)
            elif rep.failed:
                error_msg = str(rep.longrepr) if rep.longrepr else "Test failed"
                test_results.record_result(impl_name, test_name, False, error_msg)
            elif rep.skipped:
                skip_reason = rep.longrepr[2] if rep.longrepr else "Test skipped"
                test_results.record_skip(impl_name, test_name, skip_reason)

# Hook to save results at the end of testing
@pytest.hookimpl(trylast=True)
def pytest_sessionfinish(session, exitstatus):
    """Save test results at the end of the test session."""
    test_results.save_results()

import os
import sys
import glob
import re
import importlib.util
import traceback
import types
from typing import Dict, List, Optional, Any, Tuple

class TestUtils:
    @staticmethod
    def discover_implementation_files(directory: str = None) -> List[str]:
        """Find all implementation files in the current sandbox directory."""
        if directory is None:
            directory = os.path.dirname(os.path.abspath(__file__))

        patterns = [
            r'modified_code\d+\.py',
            r'new_code\d+\.py',
            r'original_code\.py',
            r'implementation\d*\.py'
        ]

        pattern = re.compile('|'.join(f'({p})' for p in patterns))
        implementations = []

        for file_path in glob.glob(os.path.join(directory, '*.py')):
            if pattern.search(os.path.basename(file_path)):
                implementations.append(file_path)

        # Sort files numerically
        def sort_key(path):
            filename = os.path.basename(path)
            match = re.search(r'(\d+)', filename)
            return int(match.group(1)) if match else 0

        return sorted(implementations, key=sort_key)

    @staticmethod
    def create_mock_module(file_path: str, module_name: str, error_info: str) -> types.ModuleType:
        """Create a mock module that contains error information but can still be tested."""
        # Create a new module object
        mock_module = types.ModuleType(module_name)

        # Add basic attributes
        mock_module.__file__ = file_path
        mock_module.__name__ = module_name
        mock_module.__display_name__ = module_name
        mock_module.__error__ = error_info

        # Add a dummy function that can be detected by test functions
        def dummy_function(*args, **kwargs):
            return f"Error in module: {error_info}"

        setattr(mock_module, "implementation_error", dummy_function)

        return mock_module

    @staticmethod
    def load_module(file_path: str, module_name: Optional[str] = None) -> Any:
        """
        Safely load a module from a file path with proper error handling.
        If the module has errors, return a mock module that can still be tested.
        """
        if module_name is None:
            module_name = os.path.basename(file_path).replace('.py', '')

        # Create a unique module name to avoid conflicts
        sandbox_id = os.path.basename(os.path.dirname(file_path))
        unique_module_name = f"{sandbox_id}_{module_name}"

        try:
            # First, try to read the file to check for syntax errors
            with open(file_path, 'r') as f:
                source_code = f.read()

            # Check for syntax errors by compiling the code
            try:
                compiled = compile(source_code, file_path, 'exec')
            except SyntaxError as e:
                error_msg = f"Syntax error: {str(e)}"
                print(f"Syntax error in {file_path}: {e}")
                print(f"  Line {e.lineno}, column {e.offset}: {e.text}")
                return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

            # Create the module spec
            spec = importlib.util.spec_from_file_location(unique_module_name, file_path)
            if spec is None or spec.loader is None:
                error_msg = f"Could not create spec for {file_path}"
                print(f"Error: {error_msg}")
                return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

            # Create the module object
            module = importlib.util.module_from_spec(spec)
            sys.modules[unique_module_name] = module

            # Special handling for execution errors
            try:
                # Execute the module code in a safe way
                spec.loader.exec_module(module)
                # Store the original name for reference
                module.__display_name__ = module_name
                return module
            except Exception as e:
                error_msg = f"Runtime error: {str(e)}"
                traceback_str = traceback.format_exc()
                print(f"Error executing module {file_path}: {e}")
                print(traceback_str)

                # Create a partial module that contains what we loaded before the error
                mock_module = TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

                # Copy any attributes that might have been defined before the error
                for attr_name in dir(module):
                    if not attr_name.startswith('__'):
                        try:
                            setattr(mock_module, attr_name, getattr(module, attr_name))
                        except Exception:
                            pass  # Skip attributes that can't be copied

                return mock_module

        except FileNotFoundError as e:
            error_msg = f"File not found: {str(e)}"
            print(f"Error: {error_msg}")
            return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)
        except Exception as e:
            error_msg = f"Unexpected error: {str(e)}"
            print(f"Error loading module {file_path}: {e}")
            return TestUtils.create_mock_module(file_path, unique_module_name, error_msg)

    @classmethod
    def load_all_implementations(cls, directory: str = None) -> Dict[str, Any]:
        """Load all implementation files in the directory, including those with errors."""
        if directory is None:
            directory = os.path.dirname(os.path.abspath(__file__))

        implementations = {}

        implementation_files = cls.discover_implementation_files(directory)
        if not implementation_files:
            print("WARNING: No implementation files found. Check your file naming patterns.")

        for file_path in implementation_files:
            module_name = os.path.basename(file_path).replace('.py', '')
            module = cls.load_module(file_path, module_name)

            # Always add the module, even if it has errors
            implementations[module_name] = module

            if hasattr(module, '__error__'):
                print(f"Loaded with errors: {module_name} - {module.__error__}")
            else:
                print(f"Successfully loaded: {module_name}")

        return implementations

class TestResultsManager:
    def __init__(self):
        self.results = {}
        self.sandbox_name = os.path.basename(os.path.dirname(os.path.abspath(__file__)))

    def record_result(self, impl_name: str, test_name: str, passed: bool,
                     error_msg: Optional[str] = None) -> None:
        """Record a test result for an implementation."""
        if impl_name not in self.results:
            self.results[impl_name] = {"passed": 0, "failed": 0, "skipped": 0, "errors": []}

        if passed:
            self.results[impl_name]["passed"] += 1
        else:
            self.results[impl_name]["failed"] += 1
            if error_msg:
                self.results[impl_name]["errors"].append({
                    "test": test_name,
                    "error": error_msg
                })

    def record_skip(self, impl_name: str, test_name: str,
                   reason: Optional[str] = None) -> None:
        """Record a skipped test for an implementation."""
        if impl_name not in self.results:
            self.results[impl_name] = {"passed": 0, "failed": 0, "skipped": 0, "errors": []}

        self.results[impl_name]["skipped"] += 1
        if reason:
            self.results[impl_name]["errors"].append({
                "test": test_name,
                "error": f"SKIPPED: {reason}"
            })

    def get_winner(self) -> Tuple[Optional[int], Dict]:
        """Determine the winner based on test results."""
        winner = None
        max_passed = -1

        for impl_name, results in self.results.items():
            if impl_name == "original_code":
                continue  # Skip original code when determining winner

            if results["passed"] > max_passed:
                max_passed = results["passed"]
                winner = impl_name
            # Break ties by looking at failure count
            elif results["passed"] == max_passed and winner is not None:
                if results["failed"] < self.results[winner]["failed"]:
                    winner = impl_name

        # Convert winner to numeric index if possible
        winner_index = -1
        if winner and re.match(r'modified_code\d+', winner):
            try:
                winner_index = int(re.search(r'(\d+)', winner).group(1))
            except (AttributeError, ValueError):
                pass

        return winner_index, self.results

    def save_results(self, filename: str = "test_results.json") -> None:
        """Save test results to a JSON file."""
        import json

        winner_index, results = self.get_winner()

        # Check if all tests were skipped
        all_skipped = all(
            stats["skipped"] == stats["passed"] + stats["failed"] + stats["skipped"]
            for impl_name, stats in results.items()
            if impl_name != "original_code"
        )

        output = {
            "winner": winner_index,
            "all_skipped": all_skipped,
            "results": {
                name: {
                    "passed": stats["passed"],
                    "failed": stats["failed"],
                    "skipped": stats["skipped"],
                    "total": stats["passed"] + stats["failed"] + stats["skipped"]
                }
                for name, stats in results.items()
                if not name.startswith("_")  # Skip internal items
            }
        }

        with open(filename, "w") as f:
            json.dump(output, f, indent=2)

        print(f"Test results saved to {filename}")

        return output