package Ex0

import chisel3._
import chisel3.iotesters.PeekPokeTester
import org.scalatest.{Matchers, FlatSpec}
import TestUtils._

class DotProdSpec extends FlatSpec with Matchers {
  import DotProdTests._

  val rand = new scala.util.Random(100)
  val elements = 7

  behavior of "DotProd"

  it should "Only signal valid output at end of calculation" in {
    wrapTester(
      chisel3.iotesters.Driver(() => new DotProd(elements)) { c =>
        new SignalsWhenDone(c)
      } should be(true)
    )
  }


  it should "Calculate the correct output" in {
    wrapTester(
      chisel3.iotesters.Driver(() => new DotProd(elements)) { c =>
        new CalculatesCorrectResult(c)
      } should be(true)
    )
  }


  it should "Calculate the correct output multiple times" in {
    wrapTester(
      chisel3.iotesters.Driver(() => new DotProd(elements)) { c =>
        new CalculatesMultiple(c)
      } should be(true)
    )
  }


  it should "Calculate the correct output and signal when appropriate" in {
    wrapTester(
      chisel3.iotesters.Driver(() => new DotProd(elements)) { c =>
        new CalculatesCorrectResultAndSignals(c)
      } should be(true)
    )
  }
}

object DotProdTests {

  val rand = new scala.util.Random(100)

  class SignalsWhenDone(c: DotProd) extends PeekPokeTester(c) {

    for(ii <- 0 until c.elements - 1){
      expect(c.io.outputValid, false)
      step(1)
    }
    expect(c.io.outputValid, true)
    step(1)

    for(ii <- 0 until c.elements - 1){
      expect(c.io.outputValid, false)
      step(1)
    }
    expect(c.io.outputValid, true)
    step(1)
  }


  class CalculatesCorrectResult(c: DotProd) extends PeekPokeTester(c) {

    val inputsA = List.fill(c.elements)(rand.nextInt(10))
    val inputsB = List.fill(c.elements)(rand.nextInt(10))
    println("runnign dot prod calc with inputs:")
    println(inputsA.mkString("[", "] [", "]"))
    println(inputsB.mkString("[", "] [", "]"))
    val expectedOutput = (for ((a, b) <- inputsA zip inputsB) yield a * b) sum

    for(ii <- 0 until c.elements){
      poke(c.io.dataInA, inputsA(ii))
      poke(c.io.dataInB, inputsB(ii))
      if(ii == c.elements - 1)
        expect(c.io.dataOut, expectedOutput)
      step(1)
    }
  }


  class CalculatesMultiple(c: DotProd) extends PeekPokeTester(c) {

    val inputsA = List.fill(c.elements)(rand.nextInt(10))
    val inputsB = List.fill(c.elements)(rand.nextInt(10))
    val inputsC = List.fill(c.elements)(rand.nextInt(10))
    val inputsD = List.fill(c.elements)(rand.nextInt(10))

    println("runnign dot prod calc with multiple inputs:")
    println(inputsA.mkString("[", "] [", "]"))
    println(inputsB.mkString("[", "] [", "]"))
    println()
    println(inputsC.mkString("[", "] [", "]"))
    println(inputsD.mkString("[", "] [", "]"))
    val expectedOutput1 = (for ((a, b) <- inputsA zip inputsB) yield a * b) sum
    val expectedOutput2 = (for ((a, b) <- inputsC zip inputsD) yield a * b) sum

    for(ii <- 0 until c.elements){
      poke(c.io.dataInA, inputsA(ii))
      poke(c.io.dataInB, inputsB(ii))
      if(ii == c.elements - 1)
        expect(c.io.dataOut, expectedOutput1)
      step(1)
    }

    for(ii <- 0 until c.elements){
      poke(c.io.dataInA, inputsC(ii))
      poke(c.io.dataInB, inputsD(ii))
      if(ii == c.elements - 1)
        expect(c.io.dataOut, expectedOutput2)
      step(1)
    }
  }


  class CalculatesCorrectResultAndSignals(c: DotProd) extends PeekPokeTester(c) {

    val inputsA = List.fill(c.elements)(rand.nextInt(10))
    val inputsB = List.fill(c.elements)(rand.nextInt(10))
    println("runnign dot prod calc with inputs:")
    println(inputsA.mkString("[", "] [", "]"))
    println(inputsB.mkString("[", "] [", "]"))
    val expectedOutput = (for ((a, b) <- inputsA zip inputsB) yield a * b) sum

    for(ii <- 0 until c.elements){
      poke(c.io.dataInA, inputsA(ii))
      poke(c.io.dataInB, inputsB(ii))
      if(ii == c.elements - 1){
        expect(c.io.dataOut, expectedOutput)
        expect(c.io.outputValid, true)
      }
      else
        expect(c.io.outputValid, false)
      step(1)
    }


    for(ii <- 0 until c.elements){
      poke(c.io.dataInA, inputsA(ii))
      poke(c.io.dataInB, inputsB(ii))
      if(ii == c.elements - 1){
        expect(c.io.dataOut, expectedOutput)
        expect(c.io.outputValid, true)
      }
      else
        expect(c.io.outputValid, false)
      step(1)
    }
  }
}
