package gov.va.med.esr.service;

import gov.va.med.esr.common.model.CommonEntityKeyFactory;
import gov.va.med.esr.common.model.financials.FinancialStatement;
import gov.va.med.esr.common.model.financials.IncomeTest;
import gov.va.med.esr.common.model.financials.Income;
import gov.va.med.esr.common.model.financials.SpouseFinancials;
import gov.va.med.esr.common.model.financials.FinancialInformation;
import gov.va.med.esr.common.model.financials.Expense;
import gov.va.med.esr.common.model.financials.Asset;
import gov.va.med.esr.common.model.lookup.AddressType;
import gov.va.med.esr.common.model.lookup.IncomeType;
import gov.va.med.esr.common.model.lookup.NameType;
import gov.va.med.esr.common.model.lookup.SSNType;
import gov.va.med.esr.common.model.lookup.Relationship;
import gov.va.med.esr.common.model.lookup.IncomeTestSource;
import gov.va.med.esr.common.model.lookup.ExpenseType;
import gov.va.med.esr.common.model.lookup.AssetType;
import gov.va.med.esr.common.model.lookup.Country;
import gov.va.med.esr.common.model.party.Address;
import gov.va.med.esr.common.model.person.id.PersonIdEntityKey;
import gov.va.med.esr.common.model.person.Name;
import gov.va.med.esr.common.model.person.Person;
import gov.va.med.esr.common.model.person.Spouse;
import gov.va.med.esr.common.model.person.SSN;
import gov.va.med.esr.common.util.AbstractServiceTestCase;
import gov.va.med.esr.common.infra.ImpreciseDate;
import gov.va.med.ps.model.PersonVPID;

import java.util.Iterator;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Calendar;
import java.math.BigDecimal;

import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException;

/**
 * Tests the retrieval/updating of multiple Person objects and ensures each returned person
 * after an update has valid identity traits present.
 * <p>
 * Note that if any validation exceptions occur, this JUnit may need to be updated to work around
 * those issues.  Or possibly, the appropriate data can be updated from the ESR UI to get it in
 * a state that will cause this JUnit to work.
 * <p>
 * TODO: When the number of threads is increased (above ~10) and the delay between threads starting is low
 * enough (~2000 or below), all the threads get started properly, but then starve at a certain point.  We didn't
 * have time to look into why this is occurring, but it could be machine resources or possibly
 * a deadlock situation.  This problem should be looked into further.
 *
 * @author Andrew Pach
 */
public class PersonConcurrentRetrievalTest extends AbstractServiceTestCase
{
    // The number of threads (i.e. simulatenous calls to PSIM) to use
    public static final int NUM_THREADS = 10;

    // Approximate number of seconds until we timeout the JUnit.  Make sure this value is high enough
    // to let the number of threads above finish successfully.
    public static final int TIMEOUT_SECS = 1800;

    // The approximate number of seconds to wait before displaying additional debugging output
    // regarding the threads
    public static final int DEBUG_INFO_SECS = 15;

    // Number of milliseconds to wait in between starting threads
    public static final int THREAD_START_DELAY_MILLIS = 1000;

    public void testGetMultiplePersons() throws Exception
    {
        // Ensure we have at least one Person Id to test with
        if (PersonVPID.VALID_PERSON_IDS.isEmpty())
        {
            fail("No Person ID values to test with.");
        }

        Iterator iter = null;
        ArrayList threadList = new ArrayList();

        // Spawn a bunch of threads for testing the getting of a valie Person
        for (int i = 0; i < NUM_THREADS; i++)
        {
            // Get the next Person Id
            if ((iter == null) || (!iter.hasNext()))
            {
                iter = PersonVPID.VALID_PERSON_IDS.iterator();
            }
            BigDecimal personId = (BigDecimal)iter.next();

            Thread personThread = new TestPersonThread(personId, getPersonService(), getLookupService(),
                getDemographicService(), getFinancialsService());
            threadList.add(personThread);
            personThread.start();
            Thread.sleep(THREAD_START_DELAY_MILLIS);
        }

        boolean finished = false;
        int timeoutCount = 0;
        int threadsNotCompleteCount = 0;
        while (!finished)
        {
            // Increment the timeout count in seconds
            timeoutCount++;

            threadsNotCompleteCount = 0;
            boolean threadsCompleted = true;
            for (Iterator iterator = threadList.iterator(); iterator.hasNext();)
            {
                TestPersonThread thread = (TestPersonThread)iterator.next();
                if (thread.isAlive())
                {
                    threadsCompleted = false;
                    threadsNotCompleteCount++;
                }
            }

            // If all the threads are completed, then we are finished
            if (threadsCompleted)
            {
                finished = true;
            }

            if (!finished)
            {
                // Timeout the JUnit if we have waited too long
                if (timeoutCount >= TIMEOUT_SECS)
                {
                    finished = true;
                }

                // Display summary debugging information every so often
                if (timeoutCount % DEBUG_INFO_SECS == 0)
                {
                    displaySummaryInfo(timeoutCount, threadList.size(), threadsNotCompleteCount);
                }

                // Sleep for 1 second before we try again
                try
                {
                    Thread.sleep(1000);
                }
                catch (Exception ex)
                {
                    ex.printStackTrace();
                }
            }
        }

        // Display the summary information
        displaySummaryInfo(timeoutCount, threadList.size(), threadsNotCompleteCount);

        int validPersonCount = 0;
        int invalidPersonCount = 0;
        int timeoutPersonCount = 0;
        int optimisticLockCount = 0;
        Map personMap = new HashMap();

        // Iterate through the threads to capture the result statistics
        for (Iterator iterator = threadList.iterator(); iterator.hasNext();)
        {
            TestPersonThread thread = (TestPersonThread)iterator.next();
            if (thread.isAlive())
            {
                // The thread timed out
                timeoutPersonId(thread.getPersonId());
                timeoutPersonCount++;
            }
            else
            {
                // Keep track of how many updates failed due to an optimistic lock exception
                if (thread.getOptimisticLockExceptionThrown())
                {
                    optimisticLockCount++;
                }

                // The thread returned so get the traits and compare them to any other thread's traits
                // that used the same Person Id
                Person threadPerson = thread.getPerson();
                Person previousPerson = (Person)personMap.get(thread.getPersonId());
                if (previousPerson == null)
                {
                    // This is the first thread for this person so store the person
                    personMap.put(thread.getPersonId(), threadPerson);
                    validPerson(threadPerson);
                    validPersonCount++;
                }
                else
                {
                    if ((personTraitsEqual(threadPerson, previousPerson)) &&
                        (thread.getPersonId().equals((BigDecimal)threadPerson.getPersonEntityKey().getKeyValue())))
                    {
                        // The previous thread's traits for this person Id match the current thread's traits
                        validPerson(threadPerson);
                        validPersonCount++;
                    }
                    else
                    {
                        // The previous thread's traits for this person Id DON'T match the current thread's traits
                        invalidPerson(threadPerson, previousPerson);
                        invalidPersonCount++;
                    }
                }
            }
        }

        displayPersonSummaryInfo(timeoutCount, validPersonCount, invalidPersonCount, timeoutPersonCount,
            optimisticLockCount);

        if (timeoutPersonCount > 0)
        {
            fail("Timed out while waiting for threads to complete.");
        }

        if (invalidPersonCount > 0)
        {
            fail(invalidPersonCount + " threads returned with invalid person traits.");
        }

        // If we get here, the test completed successfully
    }

    protected boolean personTraitsEqual(Person sourcePerson, Person targetPerson)
    {
        Name sourceName = sourcePerson.getLegalName();
        Name targetName = targetPerson.getLegalName();
        if ((sourceName.getFamilyName().equals(targetName.getFamilyName())) &&
            (sourceName.getGivenName().equals(targetName.getGivenName())) &&
            (sourcePerson.getVPIDEntityKey().equals(targetPerson.getVPIDEntityKey())) &&
            (sourcePerson.getPersonEntityKey().equals(targetPerson.getPersonEntityKey())) &&
            (sourcePerson.getOfficialSsn().getSsnText().equals(targetPerson.getOfficialSsn().getSsnText())) &&
            (sourcePerson.getGender().getName().equals(targetPerson.getGender().getName())))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    public static Person getPerson(BigDecimal personId, PersonService personService) throws Exception
    {
        PersonIdEntityKey personKey = CommonEntityKeyFactory.createPersonIdEntityKey(personId.toString());
        return personService.getPerson(personKey);
    }

    protected void validPerson(Person person)
    {
        System.out.println("Valid person returned: " + getPersonTraitsAsString(person));
    }

    protected void invalidPerson(Person currentPerson, Person prevPerson)
    {
        System.out.println("Invalid person returned.\nOne set of traits: " + getPersonTraitsAsString(currentPerson) +
            ".\nAnother set of traits: " + getPersonTraitsAsString(prevPerson));
    }

    protected String getPersonTraitsAsString(Person person)
    {
        Name name = person.getLegalName();
        return person.getVPIDEntityKey().getVPID() + ", " + person.getPersonEntityKey().getKeyValueAsString() +
            ", " + name.getGivenName() + " " + name.getFamilyName() + ", " +
            person.getOfficialSsn().getFormattedSsnText() + ", " +
            person.getGender().getName();
    }

    protected void timeoutPersonId(BigDecimal personId)
    {
        System.out.println("Thread timed out.  Passed in Person ID value of '" + personId + "'.");
    }

    protected void displaySummaryInfo(int totalSeconds, int totalThreads, int threadsNotCompleted)
    {
        System.out.println("Statistics after " + totalSeconds + " seconds.");
        int threadsComplete = totalThreads - threadsNotCompleted;
        System.out.println("Total threads: " + totalThreads);
        System.out.println("Threads complete: " + threadsComplete);
        System.out.println("Threads not complete: " + threadsNotCompleted);
        System.out.println(
            "Percent of threads complete: " + ((int)((float)threadsComplete / (float)totalThreads * 100)) + "%");
        System.out.println();
    }

    protected void displayPersonSummaryInfo(int timeoutCount, int validPersonCount, int invalidPersonCount,
        int timeoutPersonCount, int optimisticLockCount)
    {
        int totalPersonCount = validPersonCount + invalidPersonCount + timeoutPersonCount;
        System.out.println();
        System.out.println("Final Summary Information.");
        System.out.println("Total seconds to complete JUnit: " + timeoutCount);
        System.out.println("Total valid Persons: " + validPersonCount);
        System.out.println("Total invalid Persons: " + invalidPersonCount);
        System.out.println("Total threads that didn't return: " + timeoutPersonCount);
        System.out.println("Total updates that failed due to OptimisticLockException: " + optimisticLockCount);
        System.out
            .println("Percent of successful returns: " +
                ((int)((float)validPersonCount / (float)totalPersonCount * 100)) + "%");
    }
}

class TestPersonThread extends Thread
{
    private PersonService personService = null;
    private LookupService lookupService = null;
    private DemographicService demographicService = null;
    private FinancialsService financialsService = null;
    private BigDecimal personId = null;
    private Person person = null;
    private boolean optimisticLockExceptionThrown = false;

    public static final BigDecimal LOW_AMOUNT = new BigDecimal(1000);
    public static final BigDecimal HIGH_AMOUNT = new BigDecimal(100000);
    public static final Integer INCOME_YEAR = new Integer(2004);

    public TestPersonThread(BigDecimal personId, PersonService personService, LookupService lookupService,
        DemographicService demographicService, FinancialsService financialsService)
    {
        this.personId = personId;
        this.personService = personService;
        this.lookupService = lookupService;
        this.demographicService = demographicService;
        this.financialsService = financialsService;
    }

    public BigDecimal getPersonId()
    {
        return personId;
    }

    public Person getPerson()
    {
        return person;
    }

    public boolean getOptimisticLockExceptionThrown()
    {
        return optimisticLockExceptionThrown;
    }

    protected void updateAddress(Person person) throws UnknownLookupTypeException, UnknownLookupCodeException
    {
        Address permAddress = person.getPermanentAddress();
        if (permAddress == null)
        {
            permAddress = new Address();
            permAddress.setType((AddressType)lookupService.getAddressTypeByCode(
                AddressType.CODE_PERMANENT_ADDRESS.getName()));
            person.addAddress(permAddress);
        }

        permAddress.setLine1(String.valueOf(System.currentTimeMillis()) + " Current Millis Street");
        permAddress.setCity("ABINGTON");
        permAddress.setState("PA");
        permAddress.setZipCode("19001");
        permAddress.setCountry(Country.CODE_USA.getName());
    }

    protected Calendar createCalendar(int year)
    {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(Calendar.YEAR, year);
        return calendar;
    }

    protected Calendar createCalendar(int year, int month)
    {
        Calendar calendar = this.createCalendar(year);
        calendar.set(Calendar.MONTH, month - 1);
        return calendar;
    }

    protected Calendar createCalendar(int year, int month, int day)
    {
        Calendar calendar = this.createCalendar(year, month);
        calendar.set(Calendar.DAY_OF_MONTH, day);
        return calendar;
    }

    protected SSN createSSN(String text, String ssnTypeCode) throws Exception
    {
        SSN ssn = new SSN();
        ssn.setSsnText(text);
        ssn.setType(lookupService.getSSNTypeByCode(ssnTypeCode));
        return ssn;
    }

    protected void adjustIncome(FinancialInformation financialInfo, IncomeType.Code incomeTypeCode) throws Exception
    {
        Income income = financialInfo.getIncome(lookupService.getIncomeTypeByCode(incomeTypeCode.getName()));
        if (income == null)
        {
            // Create a spouse income if it doesn't yet exist
            income = new Income();
            income.setAmount(LOW_AMOUNT);
            financialInfo.setIncome(lookupService.getIncomeTypeByCode(incomeTypeCode.getName()), income);
        }

        // Modify the vet income
        BigDecimal incomeAmount = income.getAmount();
        if (incomeAmount.compareTo(LOW_AMOUNT) <= 0)
        {
            incomeAmount = HIGH_AMOUNT;
        }
        else
        {
            incomeAmount = LOW_AMOUNT;
        }
        income.setAmount(incomeAmount);
    }

    protected void adjustExpense(FinancialInformation financialInfo, ExpenseType.Code expenseTypeCode) throws Exception
    {
        Expense expense = financialInfo.getExpense(lookupService.getExpenseTypeByCode(expenseTypeCode.getName()));
        if (expense == null)
        {
            // Create a spouse income if it doesn't yet exist
            expense = new Expense();
            expense.setAmount(LOW_AMOUNT);
            financialInfo.setExpense(lookupService.getExpenseTypeByCode(expenseTypeCode.getName()), expense);
        }

        // Modify the vet income
        BigDecimal expenseAmount = expense.getAmount();
        if (expenseAmount.compareTo(LOW_AMOUNT) <= 0)
        {
            expenseAmount = HIGH_AMOUNT;
        }
        else
        {
            expenseAmount = LOW_AMOUNT;
        }
        expense.setAmount(expenseAmount);
    }

    protected void adjustAsset(FinancialInformation financialInfo, AssetType.Code assetTypeCode) throws Exception
    {
        Asset asset = financialInfo.getAsset(lookupService.getAssetTypeByCode(assetTypeCode.getName()));
        if (asset == null)
        {
            // Create a spouse income if it doesn't yet exist
            asset = new Asset();
            asset.setAmount(LOW_AMOUNT);
            financialInfo.setAsset(lookupService.getAssetTypeByCode(assetTypeCode.getName()), asset);
        }

        // Modify the vet income
        BigDecimal assetAmount = asset.getAmount();
        if (assetAmount.compareTo(LOW_AMOUNT) <= 0)
        {
            assetAmount = HIGH_AMOUNT;
        }
        else
        {
            assetAmount = LOW_AMOUNT;
        }
        asset.setAmount(assetAmount);
    }

    protected void updateFinancialInfo(Person person, Integer year) throws Exception
    {
        // Get the financial statement
        FinancialStatement fstmt = person.getFinancialStatement(year);
        if (fstmt == null)
        {
            // Create a new financial statement if one doesn't yet exist
            fstmt = new FinancialStatement();
            person.setFinancialStatement(year, fstmt);
        }
        fstmt.setIsPost2005Format(new Boolean(true));
        fstmt.setMarriedLastCalendarYear(Boolean.FALSE);

        
        // Get the spouse financials
        SpouseFinancials spouseFinancials = fstmt.getActiveSpouseFinancials();
        if (spouseFinancials == null)
        {
            // Add spouse financials if they don't yet exist
            spouseFinancials = new SpouseFinancials();
            spouseFinancials.setLivedWithPatient(Boolean.TRUE);
            fstmt.addSpouseFinancials(spouseFinancials);
        }

        // Get the spouse
        Spouse spouse = spouseFinancials.getReportedOn();
        if (spouse == null)
        {
            // Add a spouse if she doesn't yet exist
            NameType nameType = lookupService.getNameTypeByCode(NameType.LEGAL_NAME.getName());
            Name name = new Name();
            name.setFamilyName("Doe");
            name.setGivenName("Jane");
            name.setMiddleName(String.valueOf(System.currentTimeMillis()));
            name.setPrefix("Mrs.");
            name.setType(nameType);

            spouse = new Spouse();
            spouse.setName(name);
            spouse.addSsn(createSSN("111111111", SSNType.CODE_ACTIVE.getName()));
            spouse.setDob(new ImpreciseDate("01/01/1969"));
            spouse.setRelationship(lookupService.getRelationshipByCode(Relationship.CODE_SPOUSE.getName()));

            spouseFinancials.setReportedOn(spouse);
        }

        // Adjust the veteran and spouse income
        adjustIncome(fstmt, IncomeType.INCOME_TYPE_TOTAL_INCOME_FROM_EMPLOYMENT);
        adjustIncome(spouseFinancials, IncomeType.INCOME_TYPE_TOTAL_INCOME_FROM_EMPLOYMENT);
        adjustExpense(fstmt, ExpenseType.EXPENSE_TYPE_EDUCATIONAL_EXPENSES_BY_DEPENDENT);
        adjustAsset(fstmt, AssetType.CODE_CASH);
        adjustAsset(spouseFinancials, AssetType.CODE_CASH);

        // Get and update the income test
        IncomeTest test = person.getIncomeTest(year);
        if (test == null)
        {
            test = new IncomeTest();
            test.setIncomeYear(year);
            test.setEffectiveDate(createCalendar(2005, 10, 1).getTime());
            person.setIncomeTest(year, test);
        }

        IncomeTestSource testSource = (IncomeTestSource)lookupService.getIncomeTestSourceByCode(
            IncomeTestSource.CODE_HEC.getName());
        test.setSource(testSource);
        test.setAgreesToPayDeductible(Boolean.TRUE);
        test.setDiscloseFinancialInformation(Boolean.TRUE);
        
    }

    public void run()
    {
        try
        {
            System.out.println("Initiating call to PersonService to retrieve the person...");
            person = PersonConcurrentRetrievalTest.getPerson(personId, personService);
            System.out.println("PersonService call to retrieve the person returned.");

            // Update the address and financial information.  A permanent address is required for
            // GMT Address to be found when updating financials.
            updateAddress(person);
            updateFinancialInfo(person, INCOME_YEAR);

            System.out.println("Initiating call to save the updated person...");
            demographicService.updateDemographicInformation(person);
            financialsService.updateFinancialAssessment(INCOME_YEAR, person);
//            personService.save(person); // Direct way of saving person that doesn't go through rules
            System.out.println("Call to save the updated person returned.");

            System.out.println("Initiating call to PersonService to re-retrieve the person...");
            person = PersonConcurrentRetrievalTest.getPerson(personId, personService);
            System.out.println("PersonService call to re-retrieve the person returned.");
        }
        catch (HibernateOptimisticLockingFailureException ole)
        {
            optimisticLockExceptionThrown = true;
            try
            {
                System.out.println("Initiating call to PersonService to re-retrieve the person " +
                    "after OptimisticLockException...");
                person = PersonConcurrentRetrievalTest.getPerson(personId, personService);
                System.out
                    .println("PersonService call to re-retrieve the person returned after OptimisticLockException.");
            }
            catch (Exception ex)
            {
                ex.printStackTrace();
            }
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}