/*
 * Jalview - A Sequence Alignment Editor and Viewer (2.11.5.0)
 * Copyright (C) 2025 The Jalview Authors
 * 
 * This file is part of Jalview.
 * 
 * Jalview is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *  
 * Jalview 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 * The Jalview Authors are detailed in the 'AUTHORS' file.
 */
package jalview.analysis.scoremodels;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import jalview.api.analysis.ScoreModelI;
import jalview.api.analysis.SimilarityParamsI;
import jalview.datamodel.Alignment;
import jalview.datamodel.AlignmentAnnotation;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.AlignmentView;
import jalview.datamodel.Annotation;
import jalview.datamodel.Sequence;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
import jalview.gui.AlignFrame;
import jalview.gui.AlignViewport;
import jalview.gui.JvOptionPane;
import jalview.io.DataSourceType;
import jalview.io.FileLoader;
import jalview.math.MatrixI;

import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

// This class tests methods in Class SecondaryStructureDistanceModel 
public class SecondaryStructureDistanceModelTest
{

  /**
   * Verify computed distances of sequences with gap
   */
  @Test(groups = "Functional")
  public void testFindDistances_withGap()
  {
    AlignFrame af = setupAlignmentViewWithGap();
    AlignViewport viewport = af.getViewport();
    AlignmentView view = viewport.getAlignmentView(false);

    ScoreModelI sm = new SecondaryStructureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            af.alignPanel);

    /*
     * feature distance model always normalises by region width
     * gap-gap is always included (but scores zero)
     * the only variable parameter is 'includeGaps'
     */

    /*
     * include gaps
     * score = 0 + 0 + 1 + 0 = 1/4
     */
    SimilarityParamsI params = new SimilarityParams(false, true, true,
            true);
    params.setSecondaryStructureSource("3D Structures");
    MatrixI distances = sm.findDistances(view, params);
    assertEquals(distances.getValue(0, 0), 1d);
    assertEquals(distances.getValue(1, 1), 1d);
    assertEquals(distances.getValue(0, 1), 0d);
    assertEquals(distances.getValue(1, 0), 0d);

    /*
     * exclude gaps
     * score = 0 + 0 + 0 + 0 = 0/4
     */

    SimilarityParamsI params2 = new SimilarityParams(false, true, false,
            true);
    params2.setSecondaryStructureSource("3D Structures");
    MatrixI distances2 = sm.findDistances(view, params2);
    assertEquals(distances2.getValue(0, 1), 0d);
    assertEquals(distances2.getValue(1, 0), 0d);
  }

  /**
   * Verify computed distances of sequences with gap
   */
  @Test(groups = "Functional")
  public void testFindDistances_withSSUndefinedInEitherOneSeq()
  {
    AlignFrame af = setupAlignmentViewWithoutSS("either");
    AlignViewport viewport = af.getViewport();
    AlignmentView view = viewport.getAlignmentView(false);

    ScoreModelI sm = new SecondaryStructureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            af.alignPanel);

    /*
     * feature distance model always normalises by region width
     * gap-gap is always included (but scores zero)
     * the only variable parameter is 'includeGaps'
     */

    /*
     * include gaps
     * score = 0 + 0 + 2 + 2 = 2/4
     */
    SimilarityParamsI params = new SimilarityParams(false, true, true,
            true);
    params.setSecondaryStructureSource("3D Structures");
    MatrixI distances = sm.findDistances(view, params);
    assertEquals(distances.getValue(0, 0), 1d);
    assertEquals(distances.getValue(1, 1), 1d);
    assertEquals(distances.getValue(0, 1), 0d);
    assertEquals(distances.getValue(1, 0), 0d);

    /*
     * exclude gaps
     * score = 0 + 0 + 2 + 2 = 2/4
     */

    SimilarityParamsI params2 = new SimilarityParams(false, true, false,
            true);
    params2.setSecondaryStructureSource("3D Structures");
    MatrixI distances2 = sm.findDistances(view, params2);
    assertEquals(distances2.getValue(0, 1), 0d);
    assertEquals(distances2.getValue(1, 0), 0d);
  }

  /**
   * Verify computed distances of sequences with gap
   */
  @Test(groups = "Functional")
  public void testFindDistances_withSSUndefinedInBothSeqs()
  {
    AlignFrame af = setupAlignmentViewWithoutSS("both");
    AlignViewport viewport = af.getViewport();
    AlignmentView view = viewport.getAlignmentView(false);

    ScoreModelI sm = new SecondaryStructureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            af.alignPanel);

    /*
     * feature distance model always normalises by region width
     * gap-gap is always included (but scores zero)
     * the only variable parameter is 'includeGaps'
     */

    /*
     * include gaps
     * score = 0 + 0 + 2 + 2 = 2/4
     */
    SimilarityParamsI params = new SimilarityParams(false, true, true,
            true);
    params.setSecondaryStructureSource("3D Structures");
    MatrixI distances = sm.findDistances(view, params);
    assertEquals(distances.getValue(0, 0), 1d);
    assertEquals(distances.getValue(1, 1), 1d);
    assertEquals(distances.getValue(0, 1), 0d);
    assertEquals(distances.getValue(1, 0), 0d);

    /*
     * exclude gaps
     * score = 0 + 0 + 2 + 2 = 2/4
     */

    SimilarityParamsI params2 = new SimilarityParams(false, true, false,
            true);
    params2.setSecondaryStructureSource("3D Structures");
    MatrixI distances2 = sm.findDistances(view, params2);
    assertEquals(distances2.getValue(0, 1), 0d);
    assertEquals(distances2.getValue(1, 0), 0d);
  }

  /**
   * <pre>
   * Set up
   *   column      1 2 3 4 
   *        seq s1 F R K S
   *        
   *        seq s2 F S J L
   * </pre>
   * 
   * @return
   */
  protected AlignFrame setupAlignmentView(String similar)
  {
    /*
     * sequences without gaps
     */
    SequenceI s1 = new Sequence("s1", "FRKS");
    SequenceI s2 = new Sequence("s2", "FSJL");

    s1.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 4, 0f, null));
    s1.addSequenceFeature(
            new SequenceFeature("domain", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("metal", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("Pfam", null, 1, 4, 0f, null));

    /*
     * Set up secondary structure annotations
     */
    Annotation ssE = new Annotation("", "", 'E', 0);
    Annotation ssH = new Annotation("", "", 'H', 0);
    Annotation ssC = new Annotation(".", "", ' ', 0);

    Annotation[] anns1;
    Annotation[] anns2;

    /* All secondary structure annotations are similar for each column
     * Set up
    *   column      1 2 3 4 
    *        seq s1 F R K S
    *        	 ss E H S E
    *        
    *        seq s2 F S J L
    *        	 ss E H S E
    */
    if (similar == "All Similar")
    {

      anns1 = new Annotation[] { ssE, ssH, ssC, ssE };
      anns2 = new Annotation[] { ssE, ssH, ssC, ssE };

    }

    /* All secondary structure annotations are dissimilar for each column
     * Set up
     *   column      1 2 3 4 
     *        seq s1 F R K S
     *        	  ss E E C E
     *        
     *        seq s2 F S J L
     *        	  ss H E E C
     */
    else if (similar == "Not Similar")
    {

      anns1 = new Annotation[] { ssE, ssE, ssC, ssE };
      anns2 = new Annotation[] { ssH, ssH, ssE, ssC };

    }

    /* All secondary structure annotations are dissimilar for each column
     * Set up
     *   column      1 2 3 4 
     *        seq s1 F R K S
     *            ss E E C E
     *        
     *        seq s2 F S J L
     *            ss H E E C
     */
    else if (similar == "With Coil")
    {

      anns1 = new Annotation[] { ssE, ssE, null, ssE };
      anns2 = new Annotation[] { ssH, ssH, ssE, null };

    }

    /*  Set up
     *   column      1 2 3 4 
     *        seq s1 F R K S
     *        	  ss H E C E
     *        
     *        seq s2 F S J L
     *        	  ss H E E C
     */
    else
    {

      anns1 = new Annotation[] { ssH, ssE, ssC, ssE };
      anns2 = new Annotation[] { ssH, ssE, ssE, ssC };
    }

    AlignmentAnnotation ann1 = new AlignmentAnnotation(
            "Secondary Structure", "Secondary Structure", anns1);
    AlignmentAnnotation ann2 = new AlignmentAnnotation(
            "Secondary Structure", "Secondary Structure", anns2);

    s1.addAlignmentAnnotation(ann1);
    s2.addAlignmentAnnotation(ann2);

    AlignmentI al = new Alignment(new SequenceI[] { s1, s2 });
    AlignFrame af = new AlignFrame(al, 300, 300);
    af.setShowSeqFeatures(true);
    af.getFeatureRenderer().findAllFeatures(true);
    return af;
  }

  /**
   * <pre>
   * Set up
   *   column      1 2 3 4 
   *        seq s1 F R   S
   *        	  SS H E   C
   *        
   *        seq s2 F S J L
   *        	  ss H E E C
   * </pre>
   * 
   * @return
   */
  protected AlignFrame setupAlignmentViewWithGap()
  {

    SequenceI s1 = new Sequence("s1", "FR S");
    SequenceI s2 = new Sequence("s2", "FSJL");

    s1.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 3, 0f, null));
    s1.addSequenceFeature(
            new SequenceFeature("domain", null, 1, 3, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("metal", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("Pfam", null, 1, 4, 0f, null));

    Annotation ssE = new Annotation("", "", 'E', 0);
    Annotation ssH = new Annotation("", "", 'H', 0);
    Annotation ssC = new Annotation(".", "", ' ', 0);

    Annotation[] anns1;
    Annotation[] anns2;

    anns1 = new Annotation[] { ssH, ssE, ssC };
    anns2 = new Annotation[] { ssH, ssE, ssE, ssC };

    AlignmentAnnotation ann1 = new AlignmentAnnotation(
            "Secondary Structure", "Secondary Structure", anns1);
    AlignmentAnnotation ann2 = new AlignmentAnnotation(
            "Secondary Structure", "Secondary Structure", anns2);

    s1.addAlignmentAnnotation(ann1);
    s2.addAlignmentAnnotation(ann2);

    AlignmentI al = new Alignment(new SequenceI[] { s1, s2 });
    AlignFrame af = new AlignFrame(al, 300, 300);
    af.setShowSeqFeatures(true);
    af.getFeatureRenderer().findAllFeatures(true);

    return af;
  }

  protected AlignFrame setupAlignmentViewWithoutSS(String type)
  {

    SequenceI s1 = new Sequence("s1", "FR S");
    SequenceI s2 = new Sequence("s2", "FSJL");

    s1.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 3, 0f, null));
    s1.addSequenceFeature(
            new SequenceFeature("domain", null, 1, 3, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("metal", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("Pfam", null, 1, 4, 0f, null));

    if (!type.equals("both"))
    {
      Annotation ssE = new Annotation("", "", 'E', 0);
      Annotation ssH = new Annotation("", "", 'H', 0);
      Annotation ssC = new Annotation(".", "", ' ', 0);

      Annotation[] anns1;

      anns1 = new Annotation[] { ssH, ssE, ssC };

      AlignmentAnnotation ann1 = new AlignmentAnnotation(
              "Secondary Structure", "Secondary Structure", anns1);

      s1.addAlignmentAnnotation(ann1);
    }

    AlignmentI al = new Alignment(new SequenceI[] { s1, s2 });
    AlignFrame af = new AlignFrame(al, 300, 300);
    af.setShowSeqFeatures(true);
    af.getFeatureRenderer().findAllFeatures(true);
    return af;
  }

  @DataProvider(name = "testData")
  public Object[][] testData()
  {
    return new Object[][] { { "All Similar", 1d, 1d, 0d, 0d / 4 },
        { "Partially Similar", 1d, 1d, 0d, 0d },
        { "Not Similar", 1d, 1d, 0d, 0d },
        { "With Coil", 1d, 1d, 0d, 0d }, };
  }

  @Test(dataProvider = "testData", groups = { "Functional" })
  public void testFindDistances(String scenario, double expectedValue00,
          double expectedValue11, double expectedValue01,
          double expectedValue10)
  {
    AlignFrame af = setupAlignmentView(scenario);
    AlignViewport viewport = af.getViewport();
    AlignmentView view = viewport.getAlignmentView(false);

    ScoreModelI sm = new SecondaryStructureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            af.alignPanel);

    SimilarityParamsI params = new SimilarityParams(false, true, true,
            true);
    params.setSecondaryStructureSource("3D Structures");
    MatrixI distances = sm.findDistances(view, params);

    assertEquals(distances.getValue(0, 0), expectedValue00);
    assertEquals(distances.getValue(1, 1), expectedValue11);
    assertEquals(distances.getValue(0, 1), expectedValue01);
    assertEquals(distances.getValue(1, 0), expectedValue10);
  }

}
