import pytest
import torch
from torch.utils.data import Dataset, DataLoader
from simple_llm.transformer.gpt import GPT


class DummyDataset(Dataset):
    """Тестовый датасет для проверки обучения"""
    def __init__(self, vocab_size=100, seq_len=10, num_samples=100):
        self.data = torch.randint(0, vocab_size, (num_samples, seq_len))
        self.targets = torch.roll(self.data, -1, dims=1)
        
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, idx):
        return self.data[idx], self.targets[idx]


@pytest.fixture
def gpt_model():
    """Фикстура для создания тестовой модели GPT"""
    return GPT(
        vocab_size=100,
        max_seq_len=10,
        emb_size=64,
        num_heads=4,
        head_size=16,
        num_layers=2
    )


@pytest.fixture
def data_loaders():
    """Фикстура для создания тестовых DataLoader"""
    dataset = DummyDataset()
    train_loader = DataLoader(dataset, batch_size=10)
    valid_loader = DataLoader(dataset, batch_size=10)
    return train_loader, valid_loader


def test_fit_basic(gpt_model, data_loaders):
    """Тестирование базовой работы метода fit"""
    train_loader, valid_loader = data_loaders
    
    # Запускаем обучение
    gpt_model.fit(
        train_loader=train_loader,
        valid_loader=valid_loader,
        num_epoch=2,
        learning_rate=0.001
    )
    
    # Проверяем что атрибуты loss были установлены
    assert hasattr(gpt_model, 'train_loss'), "train_loss не был сохранен"
    assert hasattr(gpt_model, 'validation_loss'), "validation_loss не был сохранен"
    assert isinstance(gpt_model.train_loss, float), "train_loss должен быть числом"
    assert isinstance(gpt_model.validation_loss, float), "validation_loss должен быть числом"


@pytest.mark.parametrize("device", ["cpu", "cuda"])
def test_fit_device(gpt_model, data_loaders, device):
    """Тестирование работы fit на разных устройствах"""
    if device == "cuda" and not torch.cuda.is_available():
        pytest.skip("CUDA не доступен для тестирования")
    
    train_loader, valid_loader = data_loaders
    
    # Переносим модель на нужное устройство
    gpt_model._device = device
    gpt_model.to(device)
    
    gpt_model.fit(
        train_loader=train_loader,
        valid_loader=valid_loader,
        num_epoch=1,
        learning_rate=0.001
    )
    
    # Проверяем что модель действительно на нужном устройстве
    assert next(gpt_model.parameters()).device.type == device


def test_fit_loss_decrease(gpt_model, data_loaders):
    """Тестирование что веса изменяются в процессе обучения"""
    train_loader, valid_loader = data_loaders
    
    # Сохраняем начальные веса
    initial_state = {k: v.clone() for k, v in gpt_model.state_dict().items()}
    
    gpt_model.fit(
        train_loader=train_loader,
        valid_loader=valid_loader,
        num_epoch=3,
        learning_rate=0.01
    )
    
    # Проверяем что веса изменились
    changed = False
    for k in initial_state:
        if not torch.allclose(initial_state[k], gpt_model.state_dict()[k]):
            changed = True
            break
            
    assert changed, "Веса модели не изменились после обучения"


def test_fit_without_validation(gpt_model, data_loaders):
    """Тестирование работы без валидационного набора"""
    train_loader, _ = data_loaders
    
    gpt_model.fit(
        train_loader=train_loader,
        valid_loader=None,
        num_epoch=1,
        learning_rate=0.001
    )
    
    assert hasattr(gpt_model, 'train_loss')
    assert gpt_model.validation_loss is None

def test_fit_with_invalid_train_data(gpt_model):
    """Тестирование обработки невалидных train данных"""
    with pytest.raises(ValueError, match="train_loader не может быть None"):
        gpt_model.fit(
            train_loader=None,
            valid_loader=None,
            num_epoch=1,
            learning_rate=0.001
        )
