import tempfile
from random import sample
from typing import List, Optional

import peft
import pytest
from transformers import AutoModelForCausalLM

import vllm
from vllm.lora.request import LoRARequest

from .conftest import cleanup

MODEL_PATH = "Felladrin/Llama-68M-Chat-v1"
PROMPTS = [
    "[system] Given a target sentence construct the underlying meaning representation\nof the input sentence as a single function with attributes and attribute\nvalues. This function should describe the target string accurately and the\nfunction must be one of the following ['inform', 'request', 'give_opinion',\n'confirm', 'verify_attribute', 'suggest', 'request_explanation',\n'recommend', 'request_attribute'].\n\nThe attributes must be one of the following:\n['name', 'exp_release_date', 'release_year', 'developer', 'esrb', 'rating',\n'genres', 'player_perspective', 'has_multiplayer', 'platforms',\n'available_on_steam', 'has_linux_release', 'has_mac_release', 'specifier'] [/system] [user] Here is the target sentence:\nSpellForce 3 is a pretty bad game. The developer Grimlore Games is clearly a bunch of no-talent hacks, and 2017 was a terrible year for games anyway. [/user] [assistant]",  # noqa: E501
    "[system] Given a target sentence construct the underlying meaning representation\nof the input sentence as a single function with attributes and attribute\nvalues. This function should describe the target string accurately and the\nfunction must be one of the following ['inform', 'request', 'give_opinion',\n'confirm', 'verify_attribute', 'suggest', 'request_explanation',\n'recommend', 'request_attribute'].\n\nThe attributes must be one of the following:\n['name', 'exp_release_date', 'release_year', 'developer', 'esrb', 'rating',\n'genres', 'player_perspective', 'has_multiplayer', 'platforms',\n'available_on_steam', 'has_linux_release', 'has_mac_release', 'specifier'] [/system] [user] Here is the target sentence:\nI wanted to like Grimlore Games' 2017 entry, but in SpellForce 3 they just didn't get anything right. [/user] [assistant]",  # noqa: E501
    "[system] Given a target sentence construct the underlying meaning representation\nof the input sentence as a single function with attributes and attribute\nvalues. This function should describe the target string accurately and the\nfunction must be one of the following ['inform', 'request', 'give_opinion',\n'confirm', 'verify_attribute', 'suggest', 'request_explanation',\n'recommend', 'request_attribute'].\n\nThe attributes must be one of the following:\n['name', 'exp_release_date', 'release_year', 'developer', 'esrb', 'rating',\n'genres', 'player_perspective', 'has_multiplayer', 'platforms',\n'available_on_steam', 'has_linux_release', 'has_mac_release', 'specifier'] [/system] [user] Here is the target sentence:\nBioShock is a good role-playing, action-adventure, shooter that released for PlayStation, Xbox, and PC in 2007. It is available on Steam, and it has a Mac release but not a Linux release. [/user] [assistant]",  # noqa: E501
]


def get_lora_model(model_id: str, target_modules: List[str], rank: int):
    model = AutoModelForCausalLM.from_pretrained(model_id)
    lora_config = peft.tuners.lora.LoraConfig(target_modules, rank)
    lora_model = peft.PeftModel(model, lora_config)
    return lora_model


def do_sample(llm,
              lora_path: Optional[str] = None,
              lora_id: Optional[int] = None,
              logprobs: int = 0,
              n_tokens: int = 256):
    prompts = PROMPTS
    sampling_params = vllm.SamplingParams(temperature=0,
                                          max_tokens=n_tokens,
                                          logprobs=logprobs,
                                          stop=["[/assistant]"])
    outputs = llm.generate(
        prompts,
        sampling_params,
        lora_request=LoRARequest(str(lora_id), lora_id, lora_path)
        if lora_id else None)
    # Print the outputs.
    generated_texts = []
    generated_logprobs = []
    for output in outputs:
        prompt = output.prompt
        generated_text = output.outputs[0].text
        generated_texts.append(generated_text)
        print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
        generated_logprobs.append([
            list(logprob.keys()) for out in output.outputs
            for logprob in out.logprobs
        ])
    return generated_logprobs if logprobs else generated_texts


SUPPORTED_MODULES = [
    "qkv_proj", "o_proj", "gate_up_proj", "down_proj", "embed_tokens",
    "lm_head"
]
TARGET_MODULES_LIST = []
for length in range(2, 6):
    TARGET_MODULES_LIST.extend(
        [sample(SUPPORTED_MODULES, length) for _ in range(3)])


# Test the correctness when layer and rank are varied
# step 1: init a base model and serve with LoRA to get the reference results
# step 2: merge the same LoRA to the base model, serve the merged model
# step 3: compare the results from step 1 and step 2
@pytest.mark.parametrize("tp_size", [1])
@pytest.mark.parametrize("target_modules", TARGET_MODULES_LIST)
@pytest.mark.parametrize("rank", [8, 16, 32, 64])
def test_layer_variation_correctness(tp_size, target_modules, rank):
    llm = vllm.LLM(MODEL_PATH,
                   enable_lora=True,
                   max_num_seqs=16,
                   max_loras=4,
                   tensor_parallel_size=tp_size,
                   worker_use_ray=True)
    model = get_lora_model(MODEL_PATH, target_modules, rank)
    with tempfile.TemporaryDirectory() as tmpdir:
        model.save_pretrained(tmpdir)
        merged_probs = do_sample(llm, tmpdir, 1, logprobs=5, n_tokens=32)
    del llm
    cleanup()
    reference_id_sets = [set(prob[0]) for prob in merged_probs]

    model = get_lora_model(MODEL_PATH, target_modules, rank)
    with tempfile.TemporaryDirectory() as tmpdir:
        merged_model = model.merge_and_unload()
        merged_model.save_pretrained(tmpdir)
        llm = vllm.LLM(tmpdir,
                       tokenizer=MODEL_PATH,
                       enable_lora=False,
                       max_num_seqs=16,
                       tensor_parallel_size=tp_size,
                       worker_use_ray=True)
    probs = do_sample(llm, logprobs=5, n_tokens=32)
    del llm
    cleanup()
    # verify the top-5 tokens are identical for each token
    id_sets = [set(prob[0]) for prob in probs]
    assert id_sets == reference_id_sets
