/*
 * Copyright (c) [2016] [ <ether.camp> ]
 * This file is part of the ethereumJ library.
 *
 * The ethereumJ library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The ethereumJ library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
 */
package org.ethereum.core;

import org.ethereum.vm.DataWord;
import org.ethereum.vm.LogInfo;
import org.ethereum.vm.program.InternalTransaction;
import org.junit.Test;

import java.math.BigInteger;
import java.util.*;

import static org.apache.commons.collections4.CollectionUtils.size;
import static org.ethereum.util.ByteUtil.toHexString;
import static org.junit.Assert.*;

public class TransactionExecutionSummaryTest {


    @Test
    public void testRlpEncoding() {
        Transaction tx = randomTransaction();
        Set<DataWord> deleteAccounts = new HashSet<>(randomDataWords(10));
        List<LogInfo> logs = randomLogsInfo(5);

        final Map<DataWord, DataWord> readOnly = randomStorageEntries(20);
        final Map<DataWord, DataWord> changed = randomStorageEntries(5);
        Map<DataWord, DataWord> all = new HashMap<DataWord, DataWord>() {{
            putAll(readOnly);
            putAll(changed);
        }};

        BigInteger gasLeftover = new BigInteger("123");
        BigInteger gasRefund = new BigInteger("125");
        BigInteger gasUsed = new BigInteger("556");

        final int nestedLevelCount = 5000;
        final int countByLevel = 1;
        List<InternalTransaction> internalTransactions = randomInternalTransactions(tx, nestedLevelCount, countByLevel);

        byte[] result = randomBytes(32);


        byte[] encoded = new TransactionExecutionSummary.Builder(tx)
                .deletedAccounts(deleteAccounts)
                .logs(logs)
                .touchedStorage(all, changed)
                .gasLeftover(gasLeftover)
                .gasRefund(gasRefund)
                .gasUsed(gasUsed)
                .internalTransactions(internalTransactions)
                .result(result)
                .build()
                .getEncoded();


        TransactionExecutionSummary summary = new TransactionExecutionSummary(encoded);
        assertArrayEquals(tx.getHash(), summary.getTransactionHash());

        assertEquals(size(deleteAccounts), size(summary.getDeletedAccounts()));
        for (DataWord account : summary.getDeletedAccounts()) {
            assertTrue(deleteAccounts.contains(account));
        }

        assertEquals(size(logs), size(summary.getLogs()));
        for (int i = 0; i < logs.size(); i++) {
            assertLogInfoEquals(logs.get(i), summary.getLogs().get(i));
        }

        assertStorageEquals(all, summary.getTouchedStorage().getAll());
        assertStorageEquals(changed, summary.getTouchedStorage().getChanged());
        assertStorageEquals(readOnly, summary.getTouchedStorage().getReadOnly());

        assertEquals(gasRefund, summary.getGasRefund());
        assertEquals(gasLeftover, summary.getGasLeftover());
        assertEquals(gasUsed, summary.getGasUsed());

        assertEquals(nestedLevelCount * countByLevel, size(internalTransactions));

        assertArrayEquals(result, summary.getResult());
    }

    private static void assertStorageEquals(Map<DataWord, DataWord> expected, Map<DataWord, DataWord> actual) {
        assertNotNull(expected);
        assertNotNull(actual);
        assertEquals(expected.size(), actual.size());
        for (DataWord key : expected.keySet()) {
            DataWord actualValue = actual.get(key);
            assertNotNull(actualValue);
            assertArrayEquals(expected.get(key).getData(), actualValue.getData());
        }
    }

    private static void assertLogInfoEquals(LogInfo expected, LogInfo actual) {
        assertNotNull(expected);
        assertNotNull(actual);
        assertArrayEquals(expected.getAddress(), actual.getAddress());
        assertEquals(size(expected.getTopics()), size(actual.getTopics()));
        for (int i = 0; i < size(expected.getTopics()); i++) {
            assertArrayEquals(expected.getTopics().get(i).getData(), actual.getTopics().get(i).getData());
        }
        assertArrayEquals(expected.getData(), actual.getData());
    }

    private static Map<DataWord, DataWord> randomStorageEntries(int count) {
        Map<DataWord, DataWord> result = new HashMap<>();
        for (int i = 0; i < count; i++) {
            result.put(randomDataWord(), randomDataWord());
        }
        return result;
    }

    private static LogInfo randomLogInfo() {
        return new LogInfo(randomBytes(20), randomDataWords(5), randomBytes(8));
    }

    private static List<LogInfo> randomLogsInfo(int count) {
        List<LogInfo> result = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            result.add(randomLogInfo());
        }
        return result;
    }

    private static DataWord randomDataWord() {
        return DataWord.of(randomBytes(32));
    }

    private static DataWord randomAddress() {
        return DataWord.of(randomBytes(20));
    }

    private static List<DataWord> randomDataWords(int count) {
        List<DataWord> result = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            result.add(randomDataWord());
        }
        return result;
    }

    private static InternalTransaction randomInternalTransaction(Transaction parent, int deep, int index) {
        try {
            return new InternalTransaction(parent.getHash(), deep, index, randomBytes(1), DataWord.ZERO, DataWord.ZERO,
                    parent.getReceiveAddress(), randomBytes(20), randomBytes(2), randomBytes(64), "test note");
        } catch (StackOverflowError e) {
            System.out.println("\n !!! StackOverflowError: update your java run command with -Xss8M !!!\n");
            throw e;
        }
    }

    private static List<InternalTransaction> randomInternalTransactions(Transaction parent, int nestedLevelCount, int countByLevel) {
        List<InternalTransaction> result = new ArrayList<>();
        if (nestedLevelCount > 0) {
            for (int index = 0; index < countByLevel; index++) {
                result.add(randomInternalTransaction(parent, nestedLevelCount, index));
            }
            result.addAll(0, randomInternalTransactions(result.get(result.size() - 1), nestedLevelCount - 1, countByLevel));
        }

        return result;
    }

    private static Transaction randomTransaction() {
        Transaction transaction = Transaction.createDefault(toHexString(randomBytes(20)), new BigInteger(randomBytes(2)), new BigInteger(randomBytes(1)), null);
        transaction.sign(randomBytes(32));
        return transaction;
    }

    private static byte[] randomBytes(int len) {
        byte[] bytes = new byte[len];
        new Random().nextBytes(bytes);
        return bytes;
    }
}