{-# LANGUAGE QuasiQuotes #-}

{-

Testing strategies:

fromString . toString == id           ** Todo?

toString . fromString == almost id    ** Todo?

postgresql -> haskell -> postgresql   *  Done

haskell -> postgresql -> haskell      ** Todo?

But still,  what we really want to establish is that the two values
correspond;  for example,  a conversion that consistently added hour
when printed to a string and subtracted an hour when parsed from string
would still pass these tests.


Right now,  we are checking that 1400+ timestamps in the range of 1860 to
2060 round trip from postgresql to haskell and back in 5 different timezones.
In addition to UTC,  the four timezones were selected so that 2 have a positive
offset,  and 2 have a negative offset,   and that 2 have an offset of a
whole number of hours,  while the other two do not.

It may be worth adding a few more timezones to ensure better test coverage.

We are checking a handful of selected timestamps to ensure we hit
various corner-cases in the code,  in addition to 1400 timestamps randomly
generated with granularity of seconds down to microseconds in powers of ten.

-}

module Interval (testInterval) where

import Common
import Control.Monad(forM_, replicateM_)
import Data.Time.Compat
import Data.Time.LocalTime.Compat (CalendarDiffTime(..))
import Data.ByteString(ByteString)
import Database.PostgreSQL.Simple.SqlQQ

data IntervalTestCase = IntervalTestCase
  { label :: String
  , inputMonths :: Integer
  , inputSeconds :: NominalDiffTime
  , asText :: String
  }
  deriving (Eq, Show)

testInterval :: TestEnv -> Assertion
testInterval env@TestEnv{..} = do

  initializeTable env

  let milliseconds = 0.001
      seconds = 1
      minutes = 60 * seconds
      hours = 60 * minutes
      days = 24 * hours
      weeks = 7 * days
      months = 1
      years = 12 * months

  mapM (checkRoundTrip env)
    [ IntervalTestCase
      { label = "zero"
      , inputMonths = 0
      , inputSeconds = 0
      , asText = "PT0"
      }
    , IntervalTestCase
      { label = "1 year"
      , inputMonths = 1 * years
      , inputSeconds = 0
      , asText = "P1Y"
      }
    , IntervalTestCase
      { label = "2 months"
      , inputMonths = 2 * months
      , inputSeconds = 0
      , asText = "P2M"
      }
    , IntervalTestCase
      { label = "3 weeks"
      , inputMonths = 0
      , inputSeconds = 3 * weeks
      , asText = "P3W"
      }
    , IntervalTestCase
      { label = "4 days"
      , inputMonths = 0
      , inputSeconds = 4 * days
      , asText = "P4D"
      }
    , IntervalTestCase
      { label = "5 hours"
      , inputMonths = 0
      , inputSeconds = 5 * hours
      , asText = "PT5H"
      }
    , IntervalTestCase
      { label = "6 minutes"
      , inputMonths = 0
      , inputSeconds = 6 * minutes
      , asText = "PT6M"
      }
    , IntervalTestCase
      { label = "7 seconds"
      , inputMonths = 0
      , inputSeconds = 7 * seconds
      , asText = "PT7S"
      }
    , IntervalTestCase
      { label = "8 milliseconds"
      , inputMonths = 0
      , inputSeconds = 8 * milliseconds
      , asText = "PT0.008S"
      }
    , IntervalTestCase
      { label = "combination of intervals (day-size or bigger)"
      , inputMonths = 2 * years + 4 * months
      , inputSeconds = 3 * weeks + 5 * days
      , asText = "P2Y4M3W5D"
      }
    , IntervalTestCase
      { label = "combination of intervals (smaller than day-size)"
      , inputMonths = 0
      , inputSeconds = 18 * hours + 56 * minutes + 23 * seconds + 563 * milliseconds
      , asText = "PT18H56M23.563S"
      }
    , IntervalTestCase
      { label = "full combination of intervals"
      , inputMonths = 2 * years + 4 * months
      , inputSeconds = 3 * weeks + 5 * days + 18 * hours + 56 * minutes + 23 * seconds + 563 * milliseconds
      , asText = "P2Y4M3W5DT18H56M23.563S"
      }
    ]

  return ()

initializeTable :: TestEnv -> IO ()
initializeTable TestEnv{..} = withTransaction conn $ do
  execute_ conn
     [sql| CREATE TEMPORARY TABLE testinterval
             ( id serial, sample interval, PRIMARY KEY(id) ) |]

  return ()

checkRoundTrip :: TestEnv -> IntervalTestCase -> IO ()
checkRoundTrip TestEnv{..} IntervalTestCase{..} = do

  let input = CalendarDiffTime
        { ctMonths = inputMonths
        , ctTime = inputSeconds
        }

  [(returnedId :: Int, output :: CalendarDiffTime)] <- query conn [sql|
      INSERT INTO testinterval (sample)
      VALUES (?)
      RETURNING id, sample
    |] (Only input)

  assertBool ("CalendarDiffTime did not round-trip from Haskell to SQL and back (" ++ label ++ ")") $
      output == input

  [(Only isExpectedIso)] <- query conn [sql|
      SELECT sample = (?)::interval
      FROM testinterval
      WHERE id = ?
    |] (asText, returnedId)

  assertBool ("CalendarDiffTime inserted did not match ISO8601 equivalent \"" ++ asText ++ "\". (" ++ label ++ ")")
    isExpectedIso