// SPDX-License-Identifier: Apache-2.0

package chiselTests

import chisel3._
import chisel3.stage.{ChiselGeneratorAnnotation, ChiselStage}
import chisel3.util.Counter
import firrtl.passes.CheckInitialization.RefNotInitializedException
import firrtl.util.BackendCompilationUtilities
import org.scalatest._
import org.scalatest.matchers.should.Matchers

class InvalidateAPISpec extends ChiselPropSpec with Matchers with BackendCompilationUtilities with Utils {

  def myGenerateFirrtl(t: => Module): String = ChiselStage.emitChirrtl(t)
  def compileFirrtl(t: => Module): Unit = {
    val testDir = createTestDirectory(this.getClass.getSimpleName)

    (new ChiselStage).execute(
      Array[String]("-td", testDir.getAbsolutePath, "--compiler", "verilog"),
      Seq(ChiselGeneratorAnnotation(() => t))
    )
  }
  class TrivialInterface extends Bundle {
    val in = Input(Bool())
    val out = Output(Bool())
  }

  property("an output connected to DontCare should emit a Firrtl \"is invalid\" with Strict CompileOptions") {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithDontCare extends Module {
      val io = IO(new TrivialInterface)
      io.out := DontCare
      io.out := io.in
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithDontCare)
    firrtlOutput should include("io.out is invalid")
  }

  property("an output without a DontCare should NOT emit a Firrtl \"is invalid\" with Strict CompileOptions") {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithoutDontCare extends Module {
      val io = IO(new TrivialInterface)
      io.out := io.in
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    (firrtlOutput should not).include("is invalid")
  }

  property("an output without a DontCare should emit a Firrtl \"is invalid\" with NotStrict CompileOptions") {
    import chisel3.ExplicitCompileOptions.NotStrict
    class ModuleWithoutDontCare extends Module {
      val io = IO(new TrivialInterface)
      io.out := io.in
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    firrtlOutput should include("io is invalid")
  }

  property("a bundle with a DontCare should emit a Firrtl \"is invalid\" with Strict CompileOptions") {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithoutDontCare extends Module {
      val io = IO(new TrivialInterface)
      io <> DontCare
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    firrtlOutput should include("io.out is invalid")
    firrtlOutput should include("io.in is invalid")
  }

  property("a Vec with a DontCare should emit a Firrtl \"is invalid\" with Strict CompileOptions and bulk connect") {
    import chisel3.ExplicitCompileOptions.Strict
    val nElements = 5
    class ModuleWithoutDontCare extends Module {
      val io = IO(new Bundle {
        val outs = Output(Vec(nElements, Bool()))
      })
      io.outs <> DontCare
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    for (i <- 0 until nElements)
      firrtlOutput should include(s"io.outs[$i] is invalid")
  }

  property("a Vec with a DontCare should emit a Firrtl \"is invalid\" with Strict CompileOptions and mono connect") {
    import chisel3.ExplicitCompileOptions.Strict
    val nElements = 5
    class ModuleWithoutDontCare extends Module {
      val io = IO(new Bundle {
        val ins = Input(Vec(nElements, Bool()))
      })
      io.ins := DontCare
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    for (i <- 0 until nElements)
      firrtlOutput should include(s"io.ins[$i] is invalid")
  }

  property("a DontCare cannot be a connection sink (LHS) for := ") {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithDontCareSink extends Module {
      val io = IO(new TrivialInterface)
      DontCare := io.in
    }
    val exception = intercept[ChiselException] {
      extractCause[ChiselException] {
        ChiselStage.elaborate(new ModuleWithDontCareSink)
      }
    }
    exception.getMessage should include("DontCare cannot be a connection sink")
  }

  property("a DontCare cannot be a connection sink (LHS) for <>") {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithDontCareSink extends Module {
      val io = IO(new TrivialInterface)
      DontCare <> io.in
    }
    val exception = intercept[BiConnectException] {
      extractCause[BiConnectException] {
        ChiselStage.elaborate(new ModuleWithDontCareSink)
      }
    }
    exception.getMessage should include("DontCare cannot be a connection sink (LHS)")
  }

  property("FIRRTL should complain about partial initialization with Strict CompileOptions and conditional connect") {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithIncompleteAssignment extends Module {
      val io = IO(new Bundle {
        val out = Output(Bool())
      })
      val counter = Counter(8)
      when(counter.inc()) {
        io.out := true.B
      }
    }
    val exception = intercept[RefNotInitializedException] {
      compileFirrtl(new ModuleWithIncompleteAssignment)
    }
    exception.getMessage should include("is not fully initialized")
  }

  property(
    "FIRRTL should not complain about partial initialization with Strict CompileOptions and conditional connect after unconditional connect"
  ) {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithUnconditionalAssignment extends Module {
      val io = IO(new Bundle {
        val out = Output(Bool())
      })
      val counter = Counter(8)
      io.out := false.B
      when(counter.inc()) {
        io.out := true.B
      }
    }
    compileFirrtl(new ModuleWithUnconditionalAssignment)
  }

  property(
    "FIRRTL should not complain about partial initialization with Strict CompileOptions and conditional connect with otherwise clause"
  ) {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithConditionalAndOtherwiseAssignment extends Module {
      val io = IO(new Bundle {
        val out = Output(Bool())
      })
      val counter = Counter(8)
      when(counter.inc()) {
        io.out := true.B
      }.otherwise {
        io.out := false.B
      }
    }

    compileFirrtl(new ModuleWithConditionalAndOtherwiseAssignment)
  }

  property(
    "an output without a DontCare should NOT emit a Firrtl \"is invalid\" with overriden NotStrict CompileOptions"
  ) {
    import chisel3.ExplicitCompileOptions.NotStrict
    class ModuleWithoutDontCare extends Module {
      override val compileOptions = chisel3.ExplicitCompileOptions.NotStrict.copy(explicitInvalidate = true)
      val io = IO(new TrivialInterface)
      io.out := io.in
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    (firrtlOutput should not).include("is invalid")
  }

  property(
    "an output without a DontCare should NOT emit a Firrtl \"is invalid\" with overriden NotStrict CompileOptions module definition"
  ) {
    import chisel3.ExplicitCompileOptions.NotStrict
    abstract class ExplicitInvalidateModule
        extends Module()(chisel3.ExplicitCompileOptions.NotStrict.copy(explicitInvalidate = true))
    class ModuleWithoutDontCare extends ExplicitInvalidateModule {
      val io = IO(new TrivialInterface)
      io.out := io.in
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    (firrtlOutput should not).include("is invalid")
  }

  property("an output without a DontCare should emit a Firrtl \"is invalid\" with overriden Strict CompileOptions") {
    import chisel3.ExplicitCompileOptions.Strict
    class ModuleWithoutDontCare extends Module {
      override val compileOptions = chisel3.ExplicitCompileOptions.Strict.copy(explicitInvalidate = false)
      val io = IO(new TrivialInterface)
      io.out := io.in
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    firrtlOutput should include("is invalid")
  }

  property(
    "an output without a DontCare should emit a Firrtl \"is invalid\" with overriden Strict CompileOptions module definition"
  ) {
    import chisel3.ExplicitCompileOptions.Strict
    abstract class ImplicitInvalidateModule
        extends Module()(chisel3.ExplicitCompileOptions.NotStrict.copy(explicitInvalidate = false))
    class ModuleWithoutDontCare extends ImplicitInvalidateModule {
      val io = IO(new TrivialInterface)
      io.out := io.in
    }
    val firrtlOutput = myGenerateFirrtl(new ModuleWithoutDontCare)
    firrtlOutput should include("is invalid")
  }

  property("a clock should be able to be connected to a DontCare") {
    class ClockConnectedToDontCare extends Module {
      val foo = IO(Output(Clock()))
      foo := DontCare
    }
    myGenerateFirrtl(new ClockConnectedToDontCare) should include("foo is invalid")
  }
}
