{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Main (main) where

import Injection
import Projection

import Data.Complex (Complex ((:+)))
import Data.Dynamic (Dynamic)
import Data.Fixed (Fixed, E6)
import Data.Functor.Const (Const)
import Data.Functor.Identity (Identity)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Monoid (Dual)
import Data.Monoid (Product)
import Data.Monoid (Sum)
import Data.Monoid (All, Any)
import qualified Data.Monoid as Monoid (First, Last)
import Data.Ord (Down (..))
import Data.Ratio (Ratio, (%))
import Data.Semigroup (Max, Min)
import qualified Data.Semigroup as Semigroup (First, Last)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text.Lazy as Lazy (Text)
import Numeric.Natural (Natural)
import Test.Hspec
import Test.QuickCheck hiding (Fixed)
import Test.QuickCheck.Instances ()

main :: IO ()
main = hspec $ do
    describe "instance Injection Integer Integer" $ do
        it "is resolvable" (resolveInjection @Integer @Integer)
    describe "instance Injection String String" $ do
        it "is resolvable" (resolveInjection @String @String)
    describe "instance Injection Text Text" $ do
        it "is resolvable" (resolveInjection @Text @Text)
    describe "instance Injection Lazy.Text Lazy.Text" $ do
        it "is resolvable" (resolveInjection @Lazy.Text @Lazy.Text)
    describe "instance Retraction Integer Dynamic" $ do
        it "is resolvable" (resolveRetraction @Integer @Dynamic)
        it "is the left inverse of inject" (lawLeftInverse @Integer @Dynamic)
    describe "instance Injection (Maybe Integer) [Integer]" $ do
        it "is resolvable" (resolveInjection @(Maybe Integer) @[Integer])
        it "outputs a value with the same length as the input" $ do
            let test = inject @(Maybe Integer) @[Integer]
            length (test Nothing) `shouldBe` 0
            length (test (Just 1)) `shouldBe` 1
        it "is injective" (lawInjective @(Maybe Integer) @[Integer])
    describe "instance Retraction (Maybe Integer) [Integer]" $ do
        it "is resolvable" (resolveRetraction @(Maybe Integer) @[Integer])
        it "is the left inverse of inject" (lawLeftInverse @(Maybe Integer) @[Integer])
    describe "instance Injection Natural Integer" $ do
        it "is resolvable" (resolveInjection @Natural @Integer)
        it "injects the value itself" $ do
            let test = inject @Natural @Integer
            test 0 `shouldBe` 0
            test 1 `shouldBe` 1
            test 2 `shouldBe` 2
        it "is injective" (lawInjective @Natural @Integer)
    describe "instance Retraction Natural Integer" $ do
        it "is resolvable" (resolveRetraction @Natural @Integer)
        it "is the left inverse of inject" (lawLeftInverse @Natural @Integer)
        it "is defined over non-negative integers" $ do
            retract @Natural @Integer 0 `shouldBe` Just 0
            retract @Natural @Integer 1 `shouldBe` Just 1
        it "is not defined over negative integers" $ do
            retract @Natural @Integer (-1) `shouldBe` Nothing
    describe "instance Injection Text String" $ do
        it "is resolvable" (resolveInjection @Text @String)
        it "is injective" (lawInjective @Text @String)
    describe "instance Injection Lazy.Text String" $ do
        it "is resolvable" (resolveInjection @Lazy.Text @String)
        it "is injective" (lawInjective @Lazy.Text @String)
    describe "instance Injection Text Lazy.Text" $ do
        it "is resolvable" (resolveInjection @Text @Lazy.Text)
        it "is injective" (lawInjective @Text @Lazy.Text)
    describe "instance Injection Lazy.Text Text" $ do
        it "is resolvable" (resolveInjection @Lazy.Text @Text)
        it "is injective" (lawInjective @Lazy.Text @Text)
    describe "instance Injection Integer (Fixed a)" $ do
        it "is resolvable" (resolveInjection @Integer @(Fixed E6))
        it "is injective" (lawInjective @Integer @(Fixed E6))
    describe "instance Retraction Integer (Fixed a)" $ do
        it "is resolvable" (resolveRetraction @Integer @(Fixed E6))
        it "is the left inverse of inject" (lawLeftInverse @Integer @(Fixed E6))
        it "is defined over integers" $ do
            retract @Integer @(Fixed E6) 0 `shouldBe` Just 0
            retract @Integer @(Fixed E6) 1 `shouldBe` Just 1
            retract @Integer @(Fixed E6) (-1) `shouldBe` Just (-1)
        it "is not defined over fractions" $ do
            retract @Integer @(Fixed E6) (1 / 2) `shouldBe` Nothing
    describe "instance Injection Integer (Const Integer String)" $ do
        it "is resolvable" (resolveInjection @Integer @(Const Integer String))
        it "is injective" (lawInjective @Integer @(Const Integer String))
    describe "instance Injection (Const Integer String) Integer" $ do
        it "is resolvable" (resolveInjection @(Const Integer String) @Integer)
        it "is injective" (lawInjective @(Const Integer String) @Integer)
    describe "instance Injection Integer (Ratio Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Ratio Integer))
        it "is injective" (lawInjective @Integer @(Ratio Integer))
    describe "instance Retraction Integer (Ratio Integer)" $ do
        it "is resolvable" (resolveRetraction @Integer @(Ratio Integer))
        it "is the left inverse of inject" (lawLeftInverse @Integer @(Ratio Integer))
        it "is defined over integers" $ do
            retract @Integer @(Ratio Integer) (0 % 1) `shouldBe` Just 0
            retract @Integer @(Ratio Integer) (1 % 1) `shouldBe` Just 1
            retract @Integer @(Ratio Integer) (1 % (-1)) `shouldBe` Just (-1)
        it "is not defined over fractions" $ do
            retract @Integer @(Ratio Integer) (1 % 2) `shouldBe` Nothing
    describe "instance Injection Double (Complex Double)" $ do
        it "is resolvable" (resolveInjection @Double @(Complex Double))
        it "is injective" (lawInjective @Double @(Complex Double))
    describe "instance Retraction Double (Complex Double)" $ do
        it "is resolvable" (resolveRetraction @Double @(Complex Double))
        it "is the left inverse of inject" (lawLeftInverse @Double @(Complex Double))
        it "is defined over real numbers" $ do
            retract @Double @(Complex Double) (0 :+ 0) `shouldBe` Just 0
            retract @Double @(Complex Double) (1 :+ 0) `shouldBe` Just 1
            retract @Double @(Complex Double) ((-1) :+ 0) `shouldBe` Just (-1)
        it "is not defined over imaginary and complex numbers" $ do
            retract @Double @(Complex Double) (0 :+ 1) `shouldBe` Nothing
            retract @Double @(Complex Double) (1 :+ 1) `shouldBe` Nothing
    describe "instance Injection Integer (Identity Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Identity Integer))
        it "is injective" (lawInjective @Integer @(Identity Integer))
    describe "instance Injection (Identity Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Identity Integer) @Integer)
        it "is injective" (lawInjective @(Identity Integer) @Integer)
    describe "instance Injection (NonEmpty Integer) [Integer]" $ do
        it "is resolvable" (resolveInjection @(NonEmpty Integer) @[Integer])
        it "is injective" (lawInjective @(NonEmpty Integer) @[Integer])
    describe "instance Retraction (NonEmpty Integer) [Integer]" $ do
        it "is resolvable" (resolveRetraction @(NonEmpty Integer) @[Integer])
        it "is the left inverse of inject" (lawLeftInverse @(NonEmpty Integer) @[Integer])
        it "is not defined on the empty list" $ do
            retract @(NonEmpty Integer) @[Integer] [] `shouldBe` Nothing
        it "is defined over non-empty lists" $ do
            retract @(NonEmpty Integer) @[Integer] [0] `shouldBe` Just (0 :| [])
            retract @(NonEmpty Integer) @[Integer] [0, 1] `shouldBe` Just (0 :| [1])
    describe "instance Injection Integer (Down Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Down Integer))
        it "is injective" (lawInjective @Integer @(Down Integer))
    describe "instance Injection (Down Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Down Integer) @Integer)
        it "is injective" (lawInjective @(Down Integer) @Integer)
    describe "instance Injection Integer (Product Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Product Integer))
        it "is injective" (lawInjective @Integer @(Product Integer))
    describe "instance Injection (Product Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Product Integer) @Integer)
        it "is injective" (lawInjective @(Product Integer) @Integer)
    describe "instance Injection Integer (Sum Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Sum Integer))
        it "is injective" (lawInjective @Integer @(Sum Integer))
    describe "instance Injection (Sum Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Sum Integer) @Integer)
        it "is injective" (lawInjective @(Sum Integer) @Integer)
    describe "instance Injection Integer (Dual Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Dual Integer))
        it "is injective" (lawInjective @Integer @(Dual Integer))
    describe "instance Injection (Dual Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Dual Integer) @Integer)
        it "is injective" (lawInjective @(Dual Integer) @Integer)
    describe "instance Injection Integer (Monoid.Last Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Monoid.Last Integer))
        it "is injective" (lawInjective @Integer @(Monoid.Last Integer))
    describe "instance Retraction Injection (Monoid.Last Integer)" $ do
        it "is resolvable" (resolveRetraction @Integer @(Monoid.Last Integer))
        it "is the left inverse of inject" (lawLeftInverse @Integer @(Monoid.Last Integer))
        it "is not defined over mempty" $ do
            retract @Integer @(Monoid.Last Integer) mempty `shouldBe` Nothing
    describe "instance Injection Integer (Monoid.First Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Monoid.First Integer))
        it "is injective" (lawInjective @Integer @(Monoid.First Integer))
    describe "instance Retraction Injection (Monoid.First Integer)" $ do
        it "is resolvable" (resolveRetraction @Integer @(Monoid.First Integer))
        it "is the left inverse of inject" (lawLeftInverse @Integer @(Monoid.First Integer))
        it "is not defined over mempty" $ do
            retract @Integer @(Monoid.First Integer) mempty `shouldBe` Nothing
    describe "instance Injection Integer (Semigroup.First Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Semigroup.First Integer))
        it "is injective" (lawInjective @Integer @(Semigroup.First Integer))
    describe "instance Injection (Semigroup.First Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Semigroup.First Integer) @Integer)
        it "is injective" (lawInjective @(Semigroup.First Integer) @Integer)
    describe "instance Injection Integer (Semigroup.Last Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Semigroup.Last Integer))
        it "is injective" (lawInjective @Integer @(Semigroup.Last Integer))
    describe "instance Injection (Semigroup.Last Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Semigroup.Last Integer) @Integer)
        it "is injective" (lawInjective @(Semigroup.Last Integer) @Integer)
    describe "instance Injection Integer (Max Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Max Integer))
        it "is injective" (lawInjective @Integer @(Max Integer))
    describe "instance Injection (Max Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Max Integer) @Integer)
        it "is injective" (lawInjective @(Max Integer) @Integer)
    describe "instance Injection Integer (Min Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(Min Integer))
        it "is injective" (lawInjective @Integer @(Min Integer))
    describe "instance Injection (Min Integer) Integer" $ do
        it "is resolvable" (resolveInjection @(Min Integer) @Integer)
        it "is injective" (lawInjective @(Min Integer) @Integer)
    describe "instance Injection Integer (String -> Integer)" $ do
        it "is resolvable" (resolveInjection @Integer @(String -> Integer))
    describe "instance Injection Bool Any" $ do
        it "is resolvable" (resolveInjection @Bool @Any)
        it "is injective" (lawInjective @Bool @Any)
    describe "instance Injection Any Bool" $ do
        it "is resolvable" (resolveInjection @Any @Bool)
        it "is injective" (lawInjective @Any @Bool)
    describe "instance Injection Bool All" $ do
        it "is resolvable" (resolveInjection @Bool @All)
        it "is injective" (lawInjective @Bool @All)
    describe "instance Injection All Bool" $ do
        it "is resolvable" (resolveInjection @All @Bool)
        it "is injective" (lawInjective @All @Bool)
    describe "instance Projection Integer Integer" $ do
        it "is resolvable" (resolveProjection @Integer @Integer)
        it "is surjective" (lawSurjective @Integer @Integer id)
    describe "instance Section Integer Integer" $ do
        it "is resolvable" (resolveSection @Integer @Integer)
        it "is the right inverse of project" (lawRightInverse @Integer @Integer)
    describe "instance Projection [(Integer, Integer)] (Map Integer Integer)" $ do
        it "is resolvable" (resolveProjection @[(Integer, Integer)] @(Map Integer Integer))
        it "is surjective" (lawSurjective @[(Integer, Integer)] @(Map Integer Integer) Map.toList)
    describe "instance Projection [Integer] (Set Integer)" $ do
        it "is resolvable" (resolveProjection @[Integer] @(Set Integer))
        it "is surjective" (lawSurjective @[Integer] @(Set Integer) Set.toList)

resolveInjection :: forall from into. Injection from into => Expectation
resolveInjection = seq (inject @from @into) return ()

resolveRetraction :: forall from into. Retraction from into => Expectation
resolveRetraction = seq (retract @from @into) return ()

resolveProjection :: forall whole part. Projection whole part => Expectation
resolveProjection = seq (project @whole @part) return ()

resolveSection :: forall whole part. Section whole part => Expectation
resolveSection = seq (update @whole @part) return ()

lawInjective
    :: forall from into
    .  Injection from into
    => Arbitrary from
    => (Eq from, Show from)
    => (Eq into, Show into)
    => Property
lawInjective = property $ \from1 from2 ->
    let into1 = inject @from @into from1
        into2 = inject @from @into from2
    in (from1 /= from2) ==> (into1 =/= into2)

lawLeftInverse
    :: forall from into
    .  Retraction from into
    => Arbitrary from
    => (Eq from, Show from)
    => Property
lawLeftInverse = property $ \from ->
    let into = inject @from @into from
        from' = retract @from @into into
    in Just from === from'

lawSurjective
    :: forall whole part
    .  Projection whole part
    => Arbitrary part
    => (Eq part, Show part)
    => (part -> whole)  -- ^ any total right inverse of @project@
    -> Property
lawSurjective rightInverse = property $ \part ->
    -- This test actually proves nothing: that a right inverse of 'project'
    -- merely exists proves that it is surjective. Here, we are just probing the
    -- putative right inverse.
    project (rightInverse part) === part

lawRightInverse
    :: forall whole part
    .  Section whole part
    => Arbitrary whole
    => Show whole
    => Arbitrary part
    => (Eq part, Show part)
    => Property
lawRightInverse = property $ \part whole ->
    project @whole @part (update whole part) === part

instance Arbitrary a => Arbitrary (Down a) where
    arbitrary = Down <$> arbitrary
