import copy
import os
import shutil
import tempfile
import unittest

import peft
import torch
from modelscope import Preprocessor
from modelscope.models.nlp.structbert import SbertConfig, SbertForSequenceClassification
from peft import PeftModel, inject_adapter_in_model
from peft.config import PeftConfigMixin
from peft.tuners.lora import Linear
from peft.utils import WEIGHTS_NAME
from torch import nn

from swift import AdaLoraConfig, LoraConfig, LoRAConfig, Swift, get_peft_model


class TestPeft(unittest.TestCase):

    def setUp(self):
        print(('Testing %s.%s' % (type(self).__name__, self._testMethodName)))
        self.tmp_dir = tempfile.TemporaryDirectory().name
        if not os.path.exists(self.tmp_dir):
            os.makedirs(self.tmp_dir)

    def tearDown(self):
        shutil.rmtree(self.tmp_dir)
        super().tearDown()

    def test_peft_lora_injection(self):
        model = SbertForSequenceClassification(SbertConfig())
        model2 = copy.deepcopy(model)
        lora_config = LoraConfig(target_modules=['query', 'key', 'value'])
        model = Swift.prepare_model(model, lora_config)
        model.save_pretrained(self.tmp_dir, safe_serialization=False)
        with open(os.path.join(self.tmp_dir, 'configuration.json'), 'w') as f:
            f.write('{}')
        self.assertTrue(os.path.exists(os.path.join(self.tmp_dir, WEIGHTS_NAME)))
        model2 = Swift.from_pretrained(model2, self.tmp_dir)
        state_dict = model.state_dict()
        state_dict2 = model2.state_dict()
        for key in state_dict:
            self.assertTrue(key in state_dict2)
            self.assertTrue(all(torch.isclose(state_dict[key], state_dict2[key]).flatten().detach().cpu()))

    @unittest.skip
    def test_lora_merge(self):

        def reset_lora_parameters(self, adapter_name, init_lora_weights):
            if init_lora_weights is False:
                return

            if adapter_name == 'default':
                ratio = 1.0
            elif adapter_name == 'second':
                ratio = 2.0
            else:
                ratio = 3.0

            if adapter_name in self.lora_A.keys():
                nn.init.ones_(self.lora_A[adapter_name].weight)
                self.lora_A[adapter_name].weight.data = self.lora_A[adapter_name].weight.data * ratio
                nn.init.ones_(self.lora_B[adapter_name].weight)

        Linear.reset_lora_parameters = reset_lora_parameters

        model = SbertForSequenceClassification(SbertConfig())
        lora_config = LoRAConfig(target_modules=['query', 'key', 'value'])
        model = Swift.prepare_model(model, lora_config)
        lora_config2 = LoRAConfig(target_modules=['query', 'key', 'value'])
        model = Swift.prepare_model(model, {'second': lora_config2})
        model.add_weighted_adapter(['default', 'second'],
                                   weights=[0.7, 0.3],
                                   adapter_name='test',
                                   combination_type='cat')
        self.assertTrue(model.base_model.bert.encoder.layer[0].attention.self.key.active_adapter == ['test'])

        model2 = SbertForSequenceClassification(SbertConfig())
        lora_config = LoraConfig(target_modules=['query', 'key', 'value'])
        model2 = get_peft_model(model2, lora_config)
        lora_config2 = LoraConfig(target_modules=['query', 'key', 'value'])
        inject_adapter_in_model(lora_config2, model2, adapter_name='second')
        model2.add_weighted_adapter(['default', 'second'],
                                    weights=[0.7, 0.3],
                                    adapter_name='test',
                                    combination_type='cat')
        state_dict = model.state_dict()
        state_dict2 = model2.state_dict()
        state_dict2 = {key[len('base_model.model.'):]: value for key, value in state_dict2.items() if 'lora' in key}
        for key in state_dict:
            self.assertTrue(key in state_dict2)
            self.assertTrue(all(torch.isclose(state_dict[key], state_dict2[key]).flatten().detach().cpu()))

        preprocessor = Preprocessor.from_pretrained('damo/nlp_structbert_sentence-similarity_chinese-base')
        inputs = preprocessor('how are you')
        print(model(**inputs))
        model.save_pretrained(self.tmp_dir)
        model3 = SbertForSequenceClassification(SbertConfig())
        model3 = Swift.from_pretrained(model3, self.tmp_dir)
        state_dict3 = model3.state_dict()
        for key in state_dict:
            self.assertTrue(key in state_dict3)
            self.assertTrue(all(torch.isclose(state_dict[key], state_dict3[key]).flatten().detach().cpu()))

    def test_lora_reload_by_peft(self):
        lora_config = LoRAConfig(target_modules=['query', 'key', 'value'])
        model = SbertForSequenceClassification(SbertConfig())
        model2 = copy.deepcopy(model)
        model = Swift.prepare_model(model, lora_config)
        model.save_pretrained(self.tmp_dir, peft_format=True)
        model2 = PeftModel.from_pretrained(model2, self.tmp_dir)
        state_dict = model.state_dict()
        state_dict2 = model2.state_dict()
        state_dict2 = {key[len('base_model.model.'):]: value for key, value in state_dict2.items() if 'lora' in key}
        for key in state_dict:
            self.assertTrue(key in state_dict2)
            self.assertTrue(all(torch.isclose(state_dict[key], state_dict2[key]).flatten().detach().cpu()))

    def test_peft_adalora_injection(self):
        model = SbertForSequenceClassification(SbertConfig())
        model2 = copy.deepcopy(model)
        adalora_config = AdaLoraConfig(target_modules=['query', 'key', 'value'])
        model = Swift.prepare_model(model, adalora_config)
        model.save_pretrained(self.tmp_dir, safe_serialization=False)
        with open(os.path.join(self.tmp_dir, 'configuration.json'), 'w') as f:
            f.write('{}')
        self.assertTrue(os.path.exists(os.path.join(self.tmp_dir, WEIGHTS_NAME)))
        model2 = Swift.from_pretrained(model2, self.tmp_dir)
        state_dict = model.state_dict()
        state_dict2 = model2.state_dict()
        for key in state_dict:
            self.assertTrue(key in state_dict2)
            self.assertTrue(all(torch.isclose(state_dict[key], state_dict2[key]).flatten().detach().cpu()))

    @unittest.skip
    def test_peft_lora_dtype(self):
        model = SbertForSequenceClassification(SbertConfig())
        model2 = copy.deepcopy(model)
        model3 = copy.deepcopy(model)
        lora_config = LoraConfig(target_modules=['query', 'key', 'value'], lora_dtype='fp16')
        model = Swift.prepare_model(model, lora_config)
        model.save_pretrained(self.tmp_dir, safe_serialization=False)
        self.assertTrue(os.path.exists(os.path.join(self.tmp_dir, 'additional_config.json')))
        model2 = Swift.from_pretrained(model2, self.tmp_dir)
        self.assertTrue(model2.base_model.model.bert.encoder.layer[0].attention.self.key.lora_A.default.weight.dtype ==
                        torch.float16)
        self.assertTrue(model2.peft_config['default'].lora_dtype == 'fp16')
        state_dict = model.state_dict()
        state_dict2 = model2.state_dict()
        for key in state_dict:
            self.assertTrue(key in state_dict2)
            self.assertTrue(all(torch.isclose(state_dict[key], state_dict2[key]).flatten().detach().cpu()))

        PeftConfigMixin.from_pretrained = PeftConfigMixin.from_pretrained_origin
        model3 = Swift.from_pretrained(model3, self.tmp_dir)
        self.assertTrue(model3.base_model.model.bert.encoder.layer[0].attention.self.key.lora_A.default.weight.dtype ==
                        torch.float32)
        self.assertTrue(isinstance(model3.peft_config['default'], peft.LoraConfig))
