// SPDX-License-Identifier: Apache-2.0

package firrtlTests.transforms

import org.scalatest.featurespec.AnyFeatureSpec
import org.scalatest.GivenWhenThen
import firrtl.testutils.FirrtlMatchers
import java.io.File

import firrtl.graph.DiGraph
import firrtl.analyses.InstanceKeyGraph
import firrtl.annotations.CircuitTarget
import firrtl.annotations.TargetToken.OfModule
import firrtl.transforms._
import firrtl.transforms.MustDeduplicateTransform._
import firrtl.transforms.MustDeduplicateTransform.DisjointChildren._
import firrtl.util.BackendCompilationUtilities.createTestDirectory
import firrtl.stage.{FirrtlSourceAnnotation, RunFirrtlTransformAnnotation}
import firrtl.options.{TargetDirAnnotation}
import logger.{LogLevel, LogLevelAnnotation, Logger}

class MustDedupSpec extends AnyFeatureSpec with FirrtlMatchers with GivenWhenThen {

  Feature("When you have a simple non-deduping hierarcy") {
    val text = """
                 |circuit A :
                 |  module C :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    io.out <= io.in
                 |  module C_1 :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    io.out <= and(io.in, UInt("hff"))
                 |  module B :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    inst c of C
                 |    io <= c.io
                 |  module B_1 :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    inst c of C_1
                 |    io <= c.io
                 |  module A :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    inst b of B
                 |    inst b_1 of B_1
                 |    io.out <= and(b.io.out, b_1.io.out)
                 |    b.io.in <= io.in
                 |    b_1.io.in <= io.in
    """.stripMargin
    val top = CircuitTarget("A")
    val bdedup = MustDeduplicateAnnotation(Seq(top.module("B"), top.module("B_1")))
    val igraph = InstanceKeyGraph(parse(text))

    Scenario("Full compilation should fail and dump reports to disk") {
      val testDir = createTestDirectory("must_dedup")
      val reportDir = new File(testDir, "reports")
      val annos = Seq(
        TargetDirAnnotation(testDir.toString),
        FirrtlSourceAnnotation(text),
        RunFirrtlTransformAnnotation(new MustDeduplicateTransform),
        MustDeduplicateReportDirectory(reportDir.toString),
        bdedup
      )

      a[DeduplicationFailureException] shouldBe thrownBy {
        (new firrtl.stage.FirrtlPhase).transform(annos)
      }

      reportDir should exist

      val report0 = new File(reportDir, "report_0.rpt")
      report0 should exist

      val expectedModules = Seq("B", "B_1", "C", "C_1")
      for (mod <- expectedModules) {
        new File(reportDir, s"modules/$mod.fir") should exist
      }
    }

    Scenario("Non-deduping children should give actionable debug information") {
      When("Finding dedup failures")
      val failure = findDedupFailures(Seq(OfModule("B"), OfModule("B_1")), igraph)

      Then("The children should appear as a failure candidate")
      failure.candidates should be(Seq(LikelyShouldMatch(OfModule("C"), OfModule("C_1"))))

      And("There should be a pretty DiGraph showing context")
      val got = makeDedupFailureDiGraph(failure, igraph.graph.transformNodes(_.module))
      val expected = DiGraph("A" -> "(B)", "A" -> "(B_1)", "(B)" -> "C [0]", "(B_1)" -> "C_1 [0]")
      // DiGraph uses referential equality so compare serialized form
      got.prettyTree() should be(expected.prettyTree())
    }

    Scenario("Unrelated hierarchies should give actionable debug information") {
      When("Finding dedup failures")
      val failure = findDedupFailures(Seq(OfModule("B"), OfModule("C_1")), igraph)

      Then("The failure should note the hierarchies don't match")
      failure.candidates should be(Seq(DisjointChildren(OfModule("B"), OfModule("C_1"), Left)))

      And("There should be a pretty DiGraph showing context")
      val got = makeDedupFailureDiGraph(failure, igraph.graph.transformNodes(_.module))
      val expected = DiGraph("A" -> "(B) [0]", "(B) [0]" -> "C", "B_1" -> "(C_1) [0]")
      // DiGraph uses referential equality so compare serialized form
      got.prettyTree() should be(expected.prettyTree())
    }
  }

  Feature("When you have a deep, non-deduping hierarchy") {
    // Shadow hierarchy just to get an InstanceKeyGraph which can only be made from a circuit
    val text = parse("""
                       |circuit A :
                       |  module E:
                       |   skip
                       |  module F :
                       |   skip
                       |  module F_1 :
                       |   inst e of E
                       |  module D :
                       |   skip
                       |  module D_1 :
                       |   skip
                       |  module C :
                       |    inst d of D
                       |    inst f of F
                       |  module C_1 :
                       |    inst d of D_1
                       |    inst f of F_1
                       |  module B :
                       |    inst c of C
                       |    inst e of E
                       |  module B_1 :
                       |    inst c of C_1
                       |    inst e of E
                       |  module A :
                       |    inst b of B
                       |    inst b_1 of B_1
                       |""".stripMargin)
    val igraph = InstanceKeyGraph(text)

    Scenario("Non-deduping children should give actionable debug information") {
      When("Finding dedup failures")
      val failure = findDedupFailures(Seq(OfModule("B"), OfModule("B_1")), igraph)

      Then("The children should appear as a failure candidate")
      failure.candidates should be(
        Seq(LikelyShouldMatch(OfModule("D"), OfModule("D_1")), DisjointChildren(OfModule("F"), OfModule("F_1"), Right))
      )

      And("There should be a pretty DiGraph showing context")
      val got = makeDedupFailureDiGraph(failure, igraph.graph.transformNodes(_.module))
      val expected = DiGraph(
        "A" -> "(B)",
        "A" -> "(B_1)",
        "(B)" -> "C",
        "C" -> "D [0]",
        "C" -> "F [1]",
        "(B_1)" -> "C_1",
        "C_1" -> "D_1 [0]",
        "C_1" -> "F_1 [1]",
        "F_1 [1]" -> "E",
        // These last 2 are undesirable but E is included because it's a submodule of disjoint F and F_1
        "(B)" -> "E",
        "(B_1)" -> "E"
      )
      // DiGraph uses referential equality so compare serialized form
      got.prettyTree() should be(expected.prettyTree())
    }
  }

  Feature("When you have multiple modules that should dedup, but don't") {
    // Shadow hierarchy just to get an InstanceKeyGraph which can only be made from a circuit
    val text = parse("""
                       |circuit A :
                       |  module D :
                       |    skip
                       |  module D_1 :
                       |    skip
                       |  module C :
                       |    skip
                       |  module C_1 :
                       |    skip
                       |  module B :
                       |    inst c of C
                       |    inst d of D
                       |  module B_1 :
                       |    inst c of C_1
                       |    inst d of D
                       |  module B_2 :
                       |    inst c of C
                       |    inst d of D_1
                       |  module A :
                       |    inst b of B
                       |    inst b_1 of B_1
                       |    inst b_2 of B_2
                       |""".stripMargin)
    val igraph = InstanceKeyGraph(text)

    Scenario("Non-deduping children should give actionable debug information") {
      When("Finding dedup failures")
      val failure = findDedupFailures(Seq(OfModule("B"), OfModule("B_1"), OfModule("B_2")), igraph)

      Then("The children should appear as a failure candidate")
      failure.candidates should be(
        Seq(LikelyShouldMatch(OfModule("C"), OfModule("C_1")), LikelyShouldMatch(OfModule("D"), OfModule("D_1")))
      )

      And("There should be a pretty DiGraph showing context")
      val got = makeDedupFailureDiGraph(failure, igraph.graph.transformNodes(_.module))
      val expected = DiGraph(
        "A" -> "(B)",
        "A" -> "(B_1)",
        "A" -> "(B_2)",
        "(B)" -> "C [0]",
        "(B)" -> "D [1]",
        "(B_1)" -> "C_1 [0]",
        "(B_1)" -> "D [1]",
        "(B_2)" -> "C [0]",
        "(B_2)" -> "D_1 [1]"
      )
      // DiGraph uses referential equality so compare serialized form
      got.prettyTree() should be(expected.prettyTree())
    }
  }

  Feature("When you have modules that should dedup, and they do") {
    val text = """
                 |circuit A :
                 |  module C :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    io.out <= io.in
                 |  module C_1 :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    io.out <= io.in
                 |  module B :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    inst c of C
                 |    io <= c.io
                 |  module B_1 :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    inst c of C_1
                 |    io <= c.io
                 |  module A :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    inst b of B
                 |    inst b_1 of B_1
                 |    io.out <= and(b.io.out, b_1.io.out)
                 |    b.io.in <= io.in
                 |    b_1.io.in <= io.in
    """.stripMargin
    val top = CircuitTarget("A")
    val bdedup = MustDeduplicateAnnotation(Seq(top.module("B"), top.module("B_1")))

    Scenario("Full compilation should succeed") {
      val testDir = createTestDirectory("must_dedup")
      val reportDir = new File(testDir, "reports")
      val annos = Seq(
        TargetDirAnnotation(testDir.toString),
        FirrtlSourceAnnotation(text),
        RunFirrtlTransformAnnotation(new MustDeduplicateTransform),
        MustDeduplicateReportDirectory(reportDir.toString),
        bdedup
      )

      (new firrtl.stage.FirrtlPhase).transform(annos)
    }
  }

  Feature("When you have unused modules that should dedup, and they do") {
    val text = """
                 |circuit A :
                 |  module B :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    io.out <= io.in
                 |  module B_1 :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    io.out <= io.in
                 |  module A :
                 |    output io : { flip in : UInt<8>, out : UInt<8> }
                 |    inst b of B
                 |    inst b_1 of B_1
                 |    b.io.in <= io.in
                 |    b_1.io.in <= io.in
                 |    io.out <= and(io.in, UInt(123))
    """.stripMargin
    val top = CircuitTarget("A")
    val bdedup = MustDeduplicateAnnotation(Seq(top.module("B"), top.module("B_1")))

    Scenario("MustDeduplicateAnnotation should be deleted gracefully") {
      val testDir = createTestDirectory("must_dedup")
      val annos = Seq(
        TargetDirAnnotation(testDir.toString),
        FirrtlSourceAnnotation(text),
        RunFirrtlTransformAnnotation(new MustDeduplicateTransform),
        bdedup
      )

      val resAnnos = (new firrtl.stage.FirrtlPhase).transform(annos)
      resAnnos.collectFirst { case a: MustDeduplicateTransform => a } should be(None)
    }
  }
}
