/*
 * Copyright (c) 2012 The Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package htsjdk.variant.variantcontext;

import htsjdk.tribble.SimpleFeature;
import htsjdk.variant.VariantBaseTest;
import htsjdk.variant.variantcontext.VariantContextUtils.JexlVCMatchExp;
import htsjdk.variant.vcf.VCFConstants;
import java.util.*;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 *
 * @author aaron
 * @author bimber
 * @author hyq
 *
 *
 * Test out parts of the VariantJEXLContext and GenotypeJEXLContext
 */
public class VariantJEXLContextUnitTest extends VariantBaseTest {

    private static final VariantContextUtils.JexlVCMatchExp exp = new VariantContextUtils.JexlVCMatchExp(
            "name", VariantContextUtils.engine.get().createExpression("QUAL > 500.0"));

    private static final JexlVCMatchExp missingValueExpression = new VariantContextUtils.JexlVCMatchExp(
            "Zis10", VariantContextUtils.engine.get().createExpression("Z==10"));

    // SNP alleles: A[ref]/T[alt] at chr1:10. One (crappy) sample, one (bare minimum) VC.
    private static final SimpleFeature eventLoc = new SimpleFeature("chr1", 10, 10);
    private static final Allele Aref = Allele.create("A", true);
    private static final Allele Talt = Allele.create("T");
    private static final Genotype gt = new GenotypeBuilder("DummySample", Arrays.asList(Aref, Talt))
            .phased(false)
            .DP(2)
            .noGQ()
            .noAD()
            .noPL()
            .filter("lowDP")
            .attribute("WA", "whatEver")
            .make();
    private static final VariantContext vc = new VariantContextBuilder(
                    "test", eventLoc.getContig(), eventLoc.getStart(), eventLoc.getEnd(), Arrays.asList(Aref, Talt))
            .genotypes(gt)
            .noID()
            .filter("q10")
            .attribute("attr", "notEmpty")
            .make();

    //////////////////////// testing JEXLMap ////////////////////////
    @Test
    public void testGetValue() {
        final Map<VariantContextUtils.JexlVCMatchExp, Boolean> jexlMap = getJEXLMap();

        // make sure the context has a value
        Assert.assertTrue(!jexlMap.isEmpty());
        Assert.assertEquals(jexlMap.size(), 1);

        // eval our known expression
        Assert.assertTrue(!jexlMap.get(exp));
    }

    @Test(dataProvider = "getMissingValueTestData")
    public void testMissingBehaviorThroughMatch(
            VariantContext vc,
            JexlMissingValueTreatment missingValueTreatment,
            boolean expected,
            Class<? extends Exception> expectedException) {
        if (expectedException == null) {
            Assert.assertEquals(
                    VariantContextUtils.match(vc, null, missingValueExpression, missingValueTreatment), expected);
        } else {
            Assert.assertThrows(
                    expectedException,
                    () -> VariantContextUtils.match(vc, null, missingValueExpression, missingValueTreatment));
        }
    }

    @Test(dataProvider = "getMissingValueTestData")
    public void testMissingBehavior(
            VariantContext vc,
            JexlMissingValueTreatment missingValueTreatment,
            boolean expected,
            Class<? extends Exception> expectedException) {
        final JEXLMap jexlMap =
                new JEXLMap(Collections.singletonList(missingValueExpression), vc, null, missingValueTreatment);
        if (expectedException == null) {
            Assert.assertEquals((boolean) jexlMap.get(missingValueExpression), expected);
        } else {
            Assert.assertThrows(expectedException, () -> jexlMap.get(missingValueExpression));
        }
    }

    @DataProvider
    public Object[][] getMissingValueTestData() {
        final List<Allele> alleles = Arrays.asList(Aref, Talt);
        VariantContextBuilder vcb = new VariantContextBuilder("test", "chr1", 10, 10, alleles);
        VariantContext noZ = vcb.make();
        VariantContext hasZ = vcb.attribute("Z", 0).make();

        return new Object[][] {
            {noZ, JEXLMap.DEFAULT_MISSING_VALUE_TREATMENT, false, null},
            {hasZ, JEXLMap.DEFAULT_MISSING_VALUE_TREATMENT, false, null
            }, // the value isn't missing but the expression is false
            {noZ, JexlMissingValueTreatment.TREAT_AS_MATCH, true, null},
            {hasZ, JexlMissingValueTreatment.TREAT_AS_MATCH, false, null
            }, // the value isn't missing but the expression is false
            {noZ, JexlMissingValueTreatment.TREAT_AS_MISMATCH, false, null},
            {hasZ, JexlMissingValueTreatment.TREAT_AS_MISMATCH, false, null},
            {noZ, JexlMissingValueTreatment.THROW, false, IllegalArgumentException.class},
            {hasZ, JexlMissingValueTreatment.THROW, false, null}
        };
    }

    // Testing the new 'FT' and 'isPassFT' expressions in the JEXL map
    @Test
    public void testJEXLGenotypeFilters() {

        final JexlVCMatchExp passFlag = new VariantContextUtils.JexlVCMatchExp(
                "passFlag", VariantContextUtils.engine.get().createExpression("isPassFT==1"));
        final JexlVCMatchExp passFT = new VariantContextUtils.JexlVCMatchExp(
                "FTPASS", VariantContextUtils.engine.get().createExpression("FT==\"PASS\""));
        final JexlVCMatchExp failFT = new VariantContextUtils.JexlVCMatchExp(
                "FTBadCall", VariantContextUtils.engine.get().createExpression("FT==\"BadCall\""));
        final JexlVCMatchExp AD1 = new VariantContextUtils.JexlVCMatchExp(
                "AD1", VariantContextUtils.engine.get().createExpression("g.hasAD() && g.getAD().0==1"));
        final JexlVCMatchExp AD2 = new VariantContextUtils.JexlVCMatchExp(
                "AD2", VariantContextUtils.engine.get().createExpression("g.hasAD() && g.getAD().1==2"));

        final List<JexlVCMatchExp> jexlTests = Arrays.asList(passFlag, passFT, failFT, AD1, AD2);

        final List<Allele> alleles = Arrays.asList(Aref, Talt);
        final VariantContextBuilder vcb = new VariantContextBuilder("test", "chr1", 10, 10, alleles);
        final VariantContext vcPass = vcb.filters("PASS").make();
        final VariantContext vcFail = vcb.filters("BadVariant").make();
        final GenotypeBuilder gb = new GenotypeBuilder("SAMPLE", alleles);

        final Genotype genoNull = gb.make();
        final Genotype genoPass = gb.filters("PASS").AD(new int[] {1, 2}).DP(3).make();
        final Genotype genoFail = gb.filters("BadCall").AD(null).DP(0).make();

        Map<VariantContextUtils.JexlVCMatchExp, Boolean> jexlMap;

        // Create the JEXL Maps using the combinations above of vc* and geno*
        jexlMap = new JEXLMap(jexlTests, vcPass, genoPass);
        // make sure the context has a value
        Assert.assertTrue(!jexlMap.isEmpty());
        Assert.assertEquals(jexlMap.size(), 5);
        Assert.assertTrue(jexlMap.get(passFlag));
        Assert.assertTrue(jexlMap.get(passFT));
        Assert.assertFalse(jexlMap.get(failFT));
        Assert.assertTrue(jexlMap.get(AD1));
        Assert.assertTrue(jexlMap.get(AD2));

        jexlMap = new JEXLMap(jexlTests, vcPass, genoFail);
        // make sure the context has a value
        Assert.assertTrue(!jexlMap.isEmpty());
        Assert.assertEquals(jexlMap.size(), 5);
        Assert.assertFalse(jexlMap.get(passFlag));
        Assert.assertFalse(jexlMap.get(passFT));
        Assert.assertTrue(jexlMap.get(failFT));
        Assert.assertFalse(jexlMap.get(AD1));
        Assert.assertFalse(jexlMap.get(AD2));

        // Null genotype filter is equivalent to explicit "FT==PASS"
        jexlMap = new JEXLMap(jexlTests, vcPass, genoNull);
        // make sure the context has a value
        Assert.assertTrue(!jexlMap.isEmpty());
        Assert.assertEquals(jexlMap.size(), 5);
        Assert.assertTrue(jexlMap.get(passFlag));
        Assert.assertTrue(jexlMap.get(passFT));
        Assert.assertFalse(jexlMap.get(failFT));
        Assert.assertFalse(jexlMap.get(AD1));
        Assert.assertFalse(jexlMap.get(AD2));

        // Variant-level filters should have no effect here
        jexlMap = new JEXLMap(jexlTests, vcFail, genoPass);
        // make sure the context has a value
        Assert.assertTrue(!jexlMap.isEmpty());
        Assert.assertEquals(jexlMap.size(), 5);
        Assert.assertTrue(jexlMap.get(passFlag));
        Assert.assertTrue(jexlMap.get(passFT));
        Assert.assertFalse(jexlMap.get(failFT));

        jexlMap = new JEXLMap(jexlTests, vcFail, genoFail);
        // make sure the context has a value
        Assert.assertTrue(!jexlMap.isEmpty());
        Assert.assertEquals(jexlMap.size(), 5);
        Assert.assertFalse(jexlMap.get(passFlag));
        Assert.assertFalse(jexlMap.get(passFT));
        Assert.assertTrue(jexlMap.get(failFT));

        jexlMap = new JEXLMap(jexlTests, vcFail, genoNull);
        // make sure the context has a value
        Assert.assertTrue(!jexlMap.isEmpty());
        Assert.assertEquals(jexlMap.size(), 5);
        Assert.assertTrue(jexlMap.get(passFlag));
        Assert.assertTrue(jexlMap.get(passFT));
        Assert.assertFalse(jexlMap.get(failFT));
    }

    @Test(expectedExceptions = UnsupportedOperationException.class)
    public void testContainsValue() {
        final Map<VariantContextUtils.JexlVCMatchExp, Boolean> jexlMap = getJEXLMap();

        jexlMap.containsValue(exp);
    }

    @Test(expectedExceptions = UnsupportedOperationException.class)
    public void testRemove() {
        final Map<VariantContextUtils.JexlVCMatchExp, Boolean> jexlMap = getJEXLMap();

        jexlMap.remove(exp);
    }

    @Test(expectedExceptions = UnsupportedOperationException.class)
    public void testEntrySet() {
        final Map<VariantContextUtils.JexlVCMatchExp, Boolean> jexlMap = getJEXLMap();

        jexlMap.entrySet();
    }

    @Test(expectedExceptions = UnsupportedOperationException.class)
    public void testClear() {
        final Map<VariantContextUtils.JexlVCMatchExp, Boolean> jexlMap = getJEXLMap();

        jexlMap.clear();
    }

    /**
     * @return a JEXLMap for use by actual tests
     */
    private JEXLMap getJEXLMap() {
        return new JEXLMap(Collections.singletonList(exp), vc);
    }

    //////////////////////// testing GenotypeJEXLContext and its base VariantJEXLContext ////////////////////////

    /**
     * Test the various if-else cases in {@link GenotypeJEXLContext#get(String)} and {@link VariantJEXLContext#get(String)}
     * {@link GenotypeJEXLContext#has(String)} is not tested because it simply checks if get() will return null.
     */
    @Test
    public void testVariantJEXLContextGetMethod() {

        final VariantJEXLContext jEXLContext = getJEXLContext();

        // This is not tested because there's no simple test for equality for VariantContext,
        // except exhaustive attributes testing, which is what happening below.
        //        Assert.assertEquals(jEXLContext.get("vc"), new VariantContextBuilder("test", "chr1", 10, 10,
        // Arrays.asList(Aref, Talt)).make());

        // GenotypeJEXLContext
        Assert.assertTrue(((Genotype) jEXLContext.get("g")).sameGenotype(gt, false));
        Assert.assertEquals(jEXLContext.get("isHom"), VariantJEXLContext.false_string);
        Assert.assertEquals(jEXLContext.get("isHomRef"), VariantJEXLContext.false_string);
        Assert.assertEquals(jEXLContext.get("isHomVar"), VariantJEXLContext.false_string);
        Assert.assertEquals(jEXLContext.get("isHet"), VariantJEXLContext.true_string);
        Assert.assertEquals(jEXLContext.get("isCalled"), VariantJEXLContext.true_string);
        Assert.assertEquals(jEXLContext.get("isNoCall"), VariantJEXLContext.false_string);
        Assert.assertEquals(jEXLContext.get("isMixed"), VariantJEXLContext.false_string);
        Assert.assertEquals(jEXLContext.get("isAvailable"), VariantJEXLContext.true_string);
        Assert.assertEquals(jEXLContext.get("isPassFT"), VariantJEXLContext.false_string);
        Assert.assertEquals(jEXLContext.get(VCFConstants.GENOTYPE_KEY), gt.getGenotypeString());
        Assert.assertEquals(jEXLContext.get(VCFConstants.GENOTYPE_FILTER_KEY), "lowDP");
        Assert.assertEquals(
                jEXLContext.get(VCFConstants.GENOTYPE_QUALITY_KEY),
                Integer.valueOf(VCFConstants.MISSING_GENOTYPE_QUALITY_v3));
        Assert.assertEquals(jEXLContext.get("WA"), "whatEver"); // hasAnyAttribute->getAnyAttribute
        Assert.assertEquals(jEXLContext.get("lowDP"), VariantJEXLContext.true_string); // getFilters()!=null

        // VariantJEXLContext
        Assert.assertEquals(jEXLContext.get("CHROM"), eventLoc.getContig());
        Assert.assertEquals(jEXLContext.get("POS"), eventLoc.getStart());
        Assert.assertEquals(jEXLContext.get("TYPE"), VariantContext.Type.SNP.name());
        Assert.assertEquals(jEXLContext.get("QUAL"), -10.0); // because of noGQ() when building the genotype
        Assert.assertEquals(jEXLContext.get("ALLELES"), vc.getAlleles());
        Assert.assertEquals(jEXLContext.get("N_ALLELES"), vc.getNAlleles());
        Assert.assertEquals(jEXLContext.get("FILTER"), VariantJEXLContext.true_string);
        Assert.assertEquals(jEXLContext.get("homRefCount"), 0);
        Assert.assertEquals(jEXLContext.get("homVarCount"), 0);
        Assert.assertEquals(jEXLContext.get("hetCount"), 1);
        Assert.assertEquals(jEXLContext.get("attr"), "notEmpty"); // hasAnyAttribute->getAnyAttribute
        Assert.assertEquals(jEXLContext.get("q10"), VariantJEXLContext.true_string); // getFilters()!=null

        // all if-else fall through
        Assert.assertNull(jEXLContext.get("mustBeNull"));
    }

    @Test(expectedExceptions = UnsupportedOperationException.class)
    public void testVariantJEXLContextSetMethodException() {
        getJEXLContext().set("noMatterWhat", "willBlowup");
    }

    /**
     * @return a GenotypeJEXLContext for use by actual tests
     */
    private VariantJEXLContext getJEXLContext() {
        return new GenotypeJEXLContext(vc, gt);
    }
}
