packages feed

proarrow-0.1.0.0: testing/Proarrow/Testing/Laws.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

{- HLINT ignore "Redundant id" -}

-- | Reusable law-checking properties, parameterized over any 'Testable' kind: 'testCategory',
-- 'testMonoidal', 'testBinaryProducts', 'testClosed', and friends. Wiring a new category into a
-- test suite is a 'Testable' instance plus calls to these; the @test/Props@ directory of
-- proarrow's source repository has many examples.
--
-- A @test@ returns a 'TestTree', ready for a 'Test.Tasty.testGroup'. A @prop@ returns a
-- @'Property' ()@, to be composed into a property of your own. Where both exist (e.g. 'testMonoid'
-- and 'propMonoid'), the @test@ one wraps the @prop@ one.
--
-- Many of these take an explicit witness that 'TestOb' is closed under the structure being tested
-- (e.g. that @'TestOb' (a '**' b)@ follows from @'TestOb' a@ and @'TestOb' b@). The @_@-suffixed
-- variant (e.g. 'testMonoidal_') supplies it from a 'TestObIsOb' constraint, for categories where
-- every object is a 'TestOb', typically those that leave 'TestOb' at its @'Ob'@ default.
module Proarrow.Testing.Laws where

import Control.Monad (unless, when)
import Data.Default (def)
import Data.Foldable (for_)
import Data.List (genericLength, sort)
import Numeric.Natural (Natural)
import Test.Falsify (Property, testFailed)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Falsify (TestOptions, testProperty)
import Prelude hiding (elem, fst, id, snd, (.), (>>))

import Proarrow.Adjunction (Adjunction)
import Proarrow.Adjunction qualified as Adj
import Proarrow.Category.Enriched.Dagger qualified as Dagger
import Proarrow.Category.Enriched.Finitary qualified as Finitary
import Proarrow.Category.Enriched.Finitary.Sheaf qualified as FinSheaf
import Proarrow.Category.Enriched.Finitary.Topos qualified as FinTopos
import Proarrow.Category.Enriched.Thin qualified as Thin
import Proarrow.Category.Instance.Opposite (OPPOSITE (..))
import Proarrow.Category.Instance.Prof (Prof (..))
import Proarrow.Category.Instance.Sub (Sub (..))
import Proarrow.Category.Monoidal qualified as M
import Proarrow.Category.Monoidal.Cartesian qualified as Cartesian
import Proarrow.Category.Monoidal.Closed qualified as Exponential
import Proarrow.Category.Monoidal.CompactClosed qualified as CC
import Proarrow.Category.Monoidal.CopyDiscard qualified as CopyDiscard
import Proarrow.Category.Monoidal.Distributive qualified as Distributive
import Proarrow.Category.Monoidal.Hypergraph qualified as Hypergraph
import Proarrow.Category.Monoidal.StarAutonomous qualified as SA
import Proarrow.Category.Monoidal.Strength qualified as Strength
import Proarrow.Category.Sheaf qualified as Sheaf
import Proarrow.Category.Topos qualified as Topos
import Proarrow.Colimit.BinaryCoproduct qualified as BinaryCoproduct
import Proarrow.Colimit.Coequalizer qualified as Coequalizer
import Proarrow.Colimit.Initial qualified as Initial
import Proarrow.Colimit.Pushout qualified as Pushout
import Proarrow.Core
  ( CategoryOf (..)
  , Hom
  , Profunctor (..)
  , Promonad (..)
  , lmap
  , obj
  , rmap
  , (//)
  , (:~>)
  , type (+->)
  )
import Proarrow.Functor qualified as Functor
import Proarrow.Limit.BinaryProduct qualified as BinaryProduct
import Proarrow.Limit.Equalizer qualified as Equalizer
import Proarrow.Limit.Pullback qualified as Pullback
import Proarrow.Limit.Terminal qualified as Terminal
import Proarrow.Monoid qualified as Monoid
import Proarrow.Object (pattern Objs)
import Proarrow.Optic (ExOptic, Flip, Optic)
import Proarrow.Optic.Getter (GetterFl, review, view)
import Proarrow.Profunctor.Corepresentable (Corepresentable, coindex, withObCorep)
import Proarrow.Profunctor.Instance.Composition ((:.:) (..))
import Proarrow.Profunctor.Instance.Ran (Ran (..))
import Proarrow.Profunctor.Instance.Rift (Rift (..))
import Proarrow.Profunctor.Instance.Sieve (Sieve (..))
import Proarrow.Profunctor.Instance.Yoneda (Yo (..))
import Proarrow.Profunctor.Representable (Representable, withObRep)
import Proarrow.Promonad qualified as Promonad
import Proarrow.Testing
  ( Some (..)
  , SomeProfunctorElt (..)
  , TestOb'
  , TestObIsOb
  , Testable (..)
  , TestableProfunctor (..)
  , TestableTypeP
  , TestingEqShow (..)
  , WithTestOb
  , WithTestOb2
  , WithTestObCoprod
  , WithTestObCorep
  , WithTestObDual
  , WithTestObExp
  , WithTestObProd
  , WithTestObRep
  , expect
  , genNamed
  , genOb
  , genObSmall
  , genObSuchThat
  , isGenNonEmpty
  , obFromTestOb
  , testEq
  )
import Proarrow.Testing.Laws.Run
  ( CorepresentedBy
  , RepresentedBy
  , TESTED
  , TestedP
  , Witness (..)
  , Witnesses (..)
  , testLaws
  , testLawsWith
  , testProLaws
  )
import Proarrow.Tools.Laws qualified as Laws

-- * Isomorphisms

-- | Two arrows are mutually inverse: @f . g = id@ and @g . f = id@.
propIso :: forall {k} (a :: k) b. (Testable k, TestOb a, TestOb b) => a ~> b -> b ~> a -> Property ()
propIso f g = do
  testEq "right inverse" "f . g" (f . g) "id" id
  testEq "left inverse" "g . f" (g . f) "id" id

-- | An optic is an isomorphism: its 'view' and 'review' are mutually inverse, by 'propIso'.
propIso'
  :: forall {k} c (a :: k) b
   . (Testable k, TestOb a, TestOb b, (Ob b) => c (ExOptic GetterFl b b), (Ob b) => c (ExOptic (Flip GetterFl) b b))
  => Optic c a a b b -> Property ()
propIso' o = propIso (view o) (review o)

-- | Two functions between the elements of @p a b@ and of @q c d@ are mutually inverse, at
-- generated elements.
propIsoP
  :: forall p q a b c d
   . (TestableTypeP p, TestableTypeP q, TestOb a, TestOb b, TestOb c, TestOb d)
  => (p a b -> q c d) -> (q c d -> p a b) -> Property ()
propIsoP f g = do
  p <- genNamed @(p a b) "p"
  testEq "left inverse" "g (f p)" (g (f p)) "p" p
  q <- genNamed @(q c d) "q"
  testEq "right inverse" "f (g q)" (f (g q)) "q" q

-- | Two natural transformations are mutually inverse: 'propIsoP' at generated objects, and each is
-- natural ('propNaturalTransformation').
propNaturalIsoP
  :: forall {j} {k} (p :: j +-> k) q
   . (TestableProfunctor p, TestableTypeP p, TestableProfunctor q, TestableTypeP q)
  => (p :~> q) -> (q :~> p) -> Property ()
propNaturalIsoP f g = do
  Some @a <- genOb @k
  Some @b <- genOb @j
  propIsoP @p @q @a @b f g
  propNaturalTransformation f
  propNaturalTransformation g

-- * Categories

-- | The category laws: 'id' is a left and right identity for @(.)@, and @(.)@ is associative.
testCategory :: forall k. (Testable k) => TestTree
testCategory = testLaws @'[CategoryOf] @k "Category" (CategoryW :& WNil)

-- | The laws of a dagger category: 'Laws.ProLaws' 'Dagger.DaggerProfunctor' at the hom profunctor,
-- so 'Dagger.dagger' is an involution and reverses composition.
testDagger :: forall k. (Testable k, Dagger.Dagger k) => TestTree
testDagger = testDaggerProfunctor @(Hom k)

-- | The 'Dagger.DaggerProfunctor' laws of @p@ stated as code: 'Dagger.dagger' is an involution that
-- reverses 'dimap'.
testDaggerProfunctor :: forall {k} (p :: k +-> k). (Dagger.DaggerProfunctor p, TestableProfunctor p) => TestTree
testDaggerProfunctor =
  testCategoryProLaws @Dagger.DaggerProfunctor @p defaultTestOptions "Dagger"

-- * Profunctors

-- | 'testProLaws' for a profunctor class whose laws need only the categories of @p@, with the given
-- options.
testCategoryProLaws
  :: forall {j} {k} cl (p :: j +-> k)
   . (Laws.ProLaws cl, TestableProfunctor p, cl (TestedP p :: TESTED '[CategoryOf] j +-> TESTED '[CategoryOf] k))
  => TestOptions -> String -> TestTree
testCategoryProLaws opts name = testProLaws @'[CategoryOf] @'[CategoryOf] @cl @p opts genSome name (CategoryW :& WNil) (CategoryW :& WNil)

-- | The falsify options of a plain 'testProperty', to adjust for one test, e.g. a larger
-- 'overrideMaxRatio' where a law's arrows rarely exist.
defaultTestOptions :: TestOptions
defaultTestOptions = def

-- | The profunctor laws of @p@ stated as code ('Laws.ProLaws' 'Profunctor').
testProfunctor :: forall {j} {k} (p :: j +-> k). (TestableProfunctor p) => TestTree
testProfunctor = testProfunctorWith @p defaultTestOptions

-- | 'testProfunctor' with the given falsify options for each law.
testProfunctorWith :: forall {j} {k} (p :: j +-> k). (TestableProfunctor p) => TestOptions -> TestTree
testProfunctorWith opts =
  testCategoryProLaws @Profunctor @p opts "Profunctor"

-- | 'Thin.decide' agrees with the generator: an element of @p a b@ can be generated exactly
-- when @'Thin.Holds' p a b@ decides to 'Proarrow.Category.Instance.Bool.TRU', and then (the
-- profunctor being thin) it is the decided element.
propDecidable
  :: forall {j} {k} (p :: j +-> k)
   . (Thin.DecidableProfunctor p, Testable j, Testable k, TestableTypeP p)
  => Property ()
propDecidable = do
  Some @a <- genOb @k
  Some @b <- genOb @j
  obFromTestOb @a $
    obFromTestOb @b $
      case Thin.decide @p @a @b of
        Thin.Yes x -> do
          unless (isGenNonEmpty @(p a b)) $ testFailed "decide: TRU, but no element can be generated"
          y <- genNamed @(p a b) "y"
          testEq "decide" "decide" x "y" y
        Thin.No -> when (isGenNonEmpty @(p a b)) $ testFailed "decide: FLS, but an element can be generated"

-- | A transformation @n :: p ':~>' q@ is natural: @n ('dimap' f g p) = 'dimap' f g (n p)@.
propNaturalTransformation
  :: forall {j} {k} (p :: j +-> k) q. (TestableProfunctor p, TestableProfunctor q) => p :~> q -> Property ()
propNaturalTransformation n = do
  SomeP @a @b p <- genProfunctorElt @p "p"
  -- an object with no arrow to @a@ discards the run
  Some @c <- genObSuchThat @k \(Some @c) -> isGenNonEmpty @(c ~> a)
  Some @d <- genObSuchThat @j \(Some @d) -> isGenNonEmpty @(b ~> d)
  f <- genNamed @(c ~> a) "f"
  g <- genNamed @(b ~> d) "g"
  testEq "naturality" "n (dimap f g p)" (n (dimap f g p)) "dimap f g (n p)" (dimap f g (n p))

-- | The numbering laws of a 'Finitary.Finitary' profunctor: 'Finitary.elements' has
-- 'Finitary.size' entries and is numbered in order, and 'Finitary.fromIndex' recovers any element
-- from its index ('Laws.ProLaws' 'Finitary.Finitary'), including elements the instance did not
-- itself produce. That law, @fromIndex . toIndex@, checks that 'Finitary.size' is correct and not
-- merely self-consistent, but only as far as the 'TestableType' generator is independent of the
-- instance. One defined as
-- @optGen 'Finitary.elements'@ makes it vacuous. The label names the profunctor, which nothing in
-- its type can supply.
testFinitary
  :: forall {j} {k} (p :: j +-> k)
   . (Finitary.Finitary p, TestableProfunctor p, TestableTypeP p)
  => String
  -> TestTree
testFinitary nm =
  testGroup
    ("Finitary " ++ nm)
    [ testProperty "numbering" (propNumbering @p)
    , testCategoryProLaws @Finitary.Finitary @p defaultTestOptions "laws"
    ]

-- | 'Finitary.elements' has 'Finitary.size' entries, numbered in order, and every index is below
-- 'Finitary.size'.
propNumbering
  :: forall {j} {k} (p :: j +-> k). (Finitary.Finitary p, TestableProfunctor p, TestableTypeP p) => Property ()
propNumbering = do
  Some @a <- genOb @k
  Some @b <- genOb @j
  let n = Finitary.size @p @a @b
      es = Finitary.elements @p @a @b
  unless (genericLength es == n) $
    testFailed ("size is " ++ show n ++ " but elements has " ++ show (genericLength es :: Natural) ++ " entries")
  unless (map (Finitary.toIndex @p @a @b) es == Finitary.indices n) $
    testFailed ("elements should be numbered in order, found " ++ show (map (Finitary.toIndex @p @a @b) es))
  x <- genNamed @(p a b) "x"
  -- The numbering claims every index is below 'Finitary.size', which a @Fin@-typed index would
  -- have given for free. Without this check an undersized 'Finitary.size' goes unnoticed, since
  -- the other laws only ever look at the elements it admits.
  unless (Finitary.toIndex x < n) $
    testFailed ("toIndex " ++ showP x ++ " is " ++ show (Finitary.toIndex x) ++ ", not below size " ++ show n)

-- * Functors, representability and adjunctions

-- | Check the functor laws of a 'Functor.Functor' @f@: @map id = id@ and @map (g . f) = map g . map
-- f@. The witness lifts 'TestOb' along @f@ (usually @\\ \@a r -> r@ when @'TestOb' (f a)@ follows
-- from @'TestOb' a@). Functors encoded as representable profunctors ('Functor.FunctorForRep') are
-- instead tested via their @'Proarrow.Profunctor.Representable.Rep'@ with 'testProfunctor', since
-- the profunctor laws on @Rep f@ are the functor laws on @f@.
propFunctor
  :: forall {k1} {k2} (f :: k1 -> k2)
   . (Functor.Functor f, Testable k1, Testable k2)
  => (forall (a :: k1) r. (TestOb a) => ((TestOb (f a)) => r) -> r)
  -> Property ()
propFunctor withTestObF = do
  Some @a <- genOb @k1
  Some @b <- genObSuchThat @k1 \(Some @b) -> isGenNonEmpty @(a ~> b)
  Some @c <- genObSuchThat @k1 \(Some @c) -> isGenNonEmpty @(b ~> c)
  f <- genNamed @(a ~> b) "f"
  g <- genNamed @(b ~> c) "g"
  withTestObF @a $
    withTestObF @c $
      -- 'Functor.withObF' recovers @Ob (f a)@\/@Ob (f c)@ from the functor (GHC will not extract
      -- them from the quantified @Ob' (f a)@ superclass on its own)
      Functor.withObF @f @a $
        Functor.withObF @f @c $ do
          testEq "identity" "map id" (Functor.map @f (obj @a)) "id" (obj @(f a))
          testEq
            "composition"
            "map (g . f)"
            (Functor.map @f (g . f))
            "map g . map f"
            (Functor.map @f g . Functor.map @f f)

-- | The functor laws of @f@ ('propFunctor') as a ready-made test.
testFunctor
  :: forall {k1} {k2} (f :: k1 -> k2)
   . (Functor.Functor f, Testable k1, Testable k2)
  => (forall (a :: k1) r. (TestOb a) => ((TestOb (f a)) => r) -> r)
  -> TestTree
testFunctor withTestObF = testProperty "Functor" (propFunctor @f (\ @a r -> withTestObF @a r))

testFunctor_
  :: forall {k1} {k2} (f :: k1 -> k2)
   . (Functor.Functor f, Testable k1, Testable k2, forall (a :: k1). (TestOb a) => TestOb' (f a))
  => TestTree
testFunctor_ = testFunctor @f (\r -> r)

-- | The 'M.MonoidalProfunctor' laws of @p@ stated as code ('Laws.ProLaws' 'M.MonoidalProfunctor').
-- The witnesses say how 'TestOb' is closed under the tensor of @j@ and of @k@.
testMonoidalProfunctor
  :: forall {j} {k} (p :: j +-> k)
   . (M.MonoidalProfunctor p, TestableProfunctor p, TestOb (M.Unit :: j), TestOb (M.Unit :: k))
  => WithTestOb2 j
  -> WithTestOb2 k
  -> TestTree
testMonoidalProfunctor withTestOb2J withTestOb2K =
  testProLaws @'[CategoryOf, M.Monoidal] @'[CategoryOf, M.Monoidal] @M.MonoidalProfunctor @p
    defaultTestOptions
    genSome
    "MonoidalProfunctor"
    (CategoryW :& MonoidalW (\ @a @b r -> withTestOb2J @a @b r) :& WNil)
    (CategoryW :& MonoidalW (\ @a @b r -> withTestOb2K @a @b r) :& WNil)

-- | The laws of strength of @p@ for the tensor acting on its own category, stated as code
-- ('Laws.ProLaws' @('Strength.Strong' 'M.Tensor')@). The witness says how 'TestOb' is closed under
-- the tensor.
testMonStrong
  :: forall {k} (p :: k +-> k)
   . (Strength.Strong M.Tensor p, M.Monoidal k, TestableProfunctor p, TestOb (M.Unit :: k))
  => WithTestOb2 k
  -> TestTree
testMonStrong withTestOb2 =
  testProLaws @'[CategoryOf, M.Monoidal] @'[CategoryOf, M.Monoidal] @(Strength.Strong M.Tensor) @p
    defaultTestOptions
    genSome
    "Strong Tensor"
    ws
    ws
  where
    ws = CategoryW :& MonoidalW (\ @a @b r -> withTestOb2 @a @b r) :& WNil

testMonStrong_
  :: forall {k} (p :: k +-> k). (Strength.Strong M.Tensor p, M.Monoidal k, TestableProfunctor p, TestObIsOb k) => TestTree
testMonStrong_ = testMonStrong @p (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The laws of costrength of @p@ for the tensor acting on its own category, stated as code
-- ('Laws.ProLaws' @('Strength.Costrong' 'M.Tensor')@). The witness says how 'TestOb' is closed
-- under the tensor.
testMonCostrong
  :: forall {k} (p :: k +-> k)
   . (Strength.Costrong M.Tensor p, M.Monoidal k, TestableProfunctor p, TestOb (M.Unit :: k))
  => WithTestOb2 k
  -> TestTree
testMonCostrong withTestOb2 =
  -- small objects: 'coact tensor' asks for arrows into a tensor of three of them, and a relation
  -- or matrix between such tensors grows with the product of their sizes
  testProLaws @'[CategoryOf, M.Monoidal] @'[CategoryOf, M.Monoidal] @(Strength.Costrong M.Tensor) @p
    defaultTestOptions
    genSomeSmall
    "Costrong Tensor"
    ws
    ws
  where
    ws = CategoryW :& MonoidalW (\ @a @b r -> withTestOb2 @a @b r) :& WNil

testMonCostrong_
  :: forall {k} (p :: k +-> k). (Strength.Costrong M.Tensor p, M.Monoidal k, TestableProfunctor p, TestObIsOb k) => TestTree
testMonCostrong_ = testMonCostrong @p (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The 'Representable' laws of @p@ stated as code ('Laws.ProLaws' 'Representable'): 'index' and
-- 'tabulate' are inverse and natural. The witness lifts 'TestOb' along @p '%' -@.
testRepresentable
  :: forall {j} {k} (p :: j +-> k)
   . (Representable p, TestableProfunctor p)
  => WithTestObRep j p
  -> TestTree
testRepresentable withTestObRep =
  testProLaws @'[CategoryOf] @'[CategoryOf, RepresentedBy '[CategoryOf] p] @Representable @p
    defaultTestOptions
    genSome
    "Representable"
    (CategoryW :& WNil)
    (CategoryW :& RepresentedW (CategoryW :& WNil) (\ @b r -> withTestObRep @b r) :& WNil)

testRepresentable_ :: forall {j} {k} (p :: j +-> k). (Representable p, TestableProfunctor p, TestObIsOb k) => TestTree
testRepresentable_ = testRepresentable @p (\ @b r -> withObRep @p @b r)

-- | The 'Corepresentable' laws of @p@ stated as code ('Laws.ProLaws' 'Corepresentable'): 'coindex'
-- and 'cotabulate' are inverse and natural. The witness lifts 'TestOb' along @p '%%' -@.
testCorepresentable
  :: forall {j} {k} (p :: j +-> k)
   . (Corepresentable p, TestableProfunctor p)
  => WithTestObCorep k p
  -> TestTree
testCorepresentable withTestObCorep =
  testProLaws @'[CategoryOf, CorepresentedBy '[CategoryOf] p] @'[CategoryOf] @Corepresentable @p
    defaultTestOptions
    genSome
    "Corepresentable"
    (CategoryW :& CorepresentedW (CategoryW :& WNil) (\ @a r -> withTestObCorep @a r) :& WNil)
    (CategoryW :& WNil)

testCorepresentable_
  :: forall {j} {k} (p :: j +-> k)
   . (Corepresentable p, TestableProfunctor p, TestObIsOb j)
  => TestTree
testCorepresentable_ = testCorepresentable @p (\ @a r -> withObCorep @p @a r)

-- | The 'Promonad' laws of @p@ stated as code: 'id' is a unit for composition, which is
-- associative, and both are natural.
testPromonad :: forall {k} (p :: k +-> k). (Promonad p, TestableProfunctor p) => TestTree
testPromonad =
  testCategoryProLaws @Promonad @p defaultTestOptions "Promonad"

-- | The 'Promonad.Procomonad' laws of @p@ stated as code: 'Promonad.proextract' is natural and a
-- counit for 'Promonad.produplicate'. The middle object of 'Promonad.produplicate' is only known to
-- be an object, hence 'TestObIsOb'.
testProcomonad :: forall {k} (p :: k +-> k). (Promonad.Procomonad p, TestableProfunctor p, TestObIsOb k) => TestTree
testProcomonad =
  testCategoryProLaws @Promonad.Procomonad @p defaultTestOptions "Procomonad"

-- | The zigzag laws of the adjunction between @p@ and @q@ stated as code, for elements of each:
-- 'Laws.ProLaws' @('Adj.LeftProadjoint' q)@ and @('Adj.Proadjunction' p)@. The middle object of
-- the unit is only known to be an object, hence 'TestObIsOb'.
testProadjunction
  :: forall {j} {k} (p :: j +-> k) (q :: k +-> j)
   . (Adj.Proadjunction p q, TestableProfunctor p, TestableProfunctor q, TestObIsOb j, TestObIsOb k)
  => TestTree
testProadjunction =
  testGroup
    "Proadjunction"
    [ testCategoryProLaws @(Adj.LeftProadjoint (TestedP q)) @p defaultTestOptions "left adjoint"
    , testCategoryProLaws @(Adj.Proadjunction (TestedP p)) @q defaultTestOptions "right adjoint"
    ]

-- | Check the adjunction laws of an 'Adjunction' @p@. An adjunction here is a profunctor that is
-- both 'Representable' and 'Corepresentable', with left adjoint @L = p '%%' -@ and right adjoint
-- @R = p '%' -@. It carries no laws of its own beyond theirs ('leftAdjunct'\/'rightAdjunct' are just
-- @'index' '.' 'cotabulate'@ and @'coindex' '.' 'tabulate'@), so this groups 'testCorepresentable'
-- (for @L@) and 'testRepresentable' (for @R@). The two witnesses lift 'TestOb' along @L@ and @R@
-- respectively.
testAdjunction
  :: forall {j} {k} (p :: j +-> k)
   . (Adjunction p, TestableProfunctor p)
  => WithTestObCorep k p
  -> WithTestObRep j p
  -> TestTree
testAdjunction withTestObL withTestObR =
  testGroup
    "Adjunction"
    [ testCorepresentable @p (\ @a r -> withTestObL @a r)
    , testRepresentable @p (\ @b r -> withTestObR @b r)
    ]

testAdjunction_
  :: forall {j} {k} (p :: j +-> k)
   . (Adjunction p, TestableProfunctor p, TestObIsOb j, TestObIsOb k)
  => TestTree
testAdjunction_ = testAdjunction @p (\ @a r -> withObCorep @p @a r) (\ @b r -> withObRep @p @b r)

-- * Limits and colimits

-- | Every arrow into the 'Terminal.TerminalObject' is 'Terminal.terminate', from
-- @'Proarrow.Tools.Laws.Laws' '['Terminal.HasTerminalObject']@.
testTerminalObject
  :: forall k
   . (Testable k, Terminal.HasTerminalObject k, TestOb (Terminal.TerminalObject :: k))
  => TestTree
testTerminalObject = testLaws @'[Terminal.HasTerminalObject] "Terminal object" (TerminalW @k :& WNil)

-- | Every arrow out of the 'Initial.InitialObject' is 'Initial.initiate', from
-- @'Proarrow.Tools.Laws.Laws' '['Initial.HasInitialObject']@.
testInitialObject :: forall k. (Testable k, Initial.HasInitialObject k, TestOb (Initial.InitialObject :: k)) => TestTree
testInitialObject = testLaws @'[Initial.HasInitialObject] "Initial object" (InitialW @k :& WNil)

-- | The universal property of the binary product, from
-- @'Proarrow.Tools.Laws.Laws' '['BinaryProduct.HasBinaryProducts']@: the projections recover the
-- components of @f '&&&' g@, pairing commutes with precomposition, and pairing the projections is
-- the identity.
testBinaryProducts :: forall k. (Testable k, BinaryProduct.HasBinaryProducts k) => WithTestObProd k -> TestTree
testBinaryProducts withTestObProd =
  testLaws @'[BinaryProduct.HasBinaryProducts] "Binary products" (ProductsW (\ @a @b r -> withTestObProd @a @b r) :& WNil)

testBinaryProducts_ :: forall k. (Testable k, BinaryProduct.HasBinaryProducts k, TestObIsOb k) => TestTree
testBinaryProducts_ = testBinaryProducts @k (\ @a @b r -> BinaryProduct.withObProd @k @a @b r)

-- | The universal property of the binary coproduct, dual to 'testBinaryProducts', from
-- @'Proarrow.Tools.Laws.Laws' '['BinaryCoproduct.HasBinaryCoproducts']@.
testBinaryCoproducts :: forall k. (Testable k, BinaryCoproduct.HasBinaryCoproducts k) => WithTestObCoprod k -> TestTree
testBinaryCoproducts withTestObCoprod =
  testLaws @'[BinaryCoproduct.HasBinaryCoproducts]
    "Binary coproducts"
    (CoproductsW (\ @a @b r -> withTestObCoprod @a @b r) :& WNil)

testBinaryCoproducts_ :: forall k. (Testable k, BinaryCoproduct.HasBinaryCoproducts k, TestObIsOb k) => TestTree
testBinaryCoproducts_ = testBinaryCoproducts @k (\ @a @b r -> BinaryCoproduct.withObCoprod @k @a @b r)

-- | Check that composing with an arrow /reflects/ equality: the composites agree exactly when the
-- two arrows already did. @eqComposed@ is the caller\'s comparison of the composites (a
-- conjunction, where two projections have to be checked together), and @desc@ names it for the
-- failure message.
--
-- This is the mono half of an equalizer, the epi half of a coequalizer, and the jointly-monic and
-- jointly-epic halves of a pullback and a pushout.
propReflectsEq :: (TestingEqShow x) => String -> String -> Bool -> x -> x -> Property ()
propReflectsEq label desc eqComposed k1 k2 = do
  eqDirect <- eqP k1 k2
  unless (eqComposed == eqDirect) $
    testFailed $
      "Failed " ++ label ++ ": (" ++ desc ++ ") = " ++ show eqComposed ++ " but (k1 == k2) = " ++ show eqDirect

-- | Checks the equalizer laws: the equalizer arrow @e@ equalizes @f@ and @g@; any @h@ that factors
-- through @e@ (generated as @e . p@) is recovered by 'Equalizer.factorEqualizer'; and @e@ is mono.
--
-- The equalizer object is not computed by a type family, so its 'Ob' comes from the arrow via
-- 'Objs', and @withTestOb@ only has to bridge that one 'Ob' to 'TestOb'.
testEqualizers :: forall k. (Testable k, Equalizer.HasEqualizers k) => WithTestOb k -> TestTree
testEqualizers withTestOb = testProperty "Equalizers" $ do
  Some @a <- genOb @k
  Some @b <- genOb @k
  f <- genNamed @(a ~> b) "f"
  g <- genNamed @(a ~> b) "g"
  Equalizer.equalize f g \ @e ee@Objs -> withTestOb @e do
    testEq "equalizing" "f . e" (f . ee) "g . e" (g . ee)
    Some @z <- genOb @k
    p <- genNamed @(z ~> e) "p"
    let h = ee . p
        factored = Equalizer.factorEqualizer ee h
    testEq "factorization" "e . factored" (ee . factored) "h" h
    -- The half the constructed @h@ cannot reach: an /arbitrary/ arrow that happens to equalize
    -- must factor too. Without it an undersized equalizer (one keeping too few elements)
    -- satisfies everything above, since every arrow it is ever handed was built through it.
    m <- genNamed @(z ~> a) "m"
    equalizes <- eqP (f . m) (g . m)
    when equalizes $
      testEq "existence" "e . factorEqualizer e m" (ee . Equalizer.factorEqualizer ee m) "m" m
    k1 <- genNamed @(z ~> e) "k1"
    k2 <- genNamed @(z ~> e) "k2"
    eqComposed <- eqP (ee . k1) (ee . k2)
    propReflectsEq "mono" "e . k1 == e . k2" eqComposed k1 k2

testEqualizers_ :: forall k. (Testable k, Equalizer.HasEqualizers k, TestObIsOb k) => TestTree
testEqualizers_ = testEqualizers @k (\r -> r)

-- | Checks the coequalizer laws, dual to 'testEqualizers': the coequalizer arrow @c@ coequalizes @f@
-- and @g@; any @h@ that factors through @c@ (built here as @p . c@ for an arbitrary @p@, so the
-- precondition holds by construction) is correctly recovered by 'Coequalizer.factorCoequalizer'; and
-- @c@ is epi (post-composing with it on the right reflects equality).
testCoequalizers :: forall k. (Testable k, Coequalizer.HasCoequalizers k) => WithTestOb k -> TestTree
testCoequalizers withTestOb = testProperty "Coequalizers" $ do
  Some @a <- genOb @k
  Some @b <- genOb @k
  f <- genNamed @(a ~> b) "f"
  g <- genNamed @(a ~> b) "g"
  Coequalizer.coequalize f g \ @c cq@Objs -> withTestOb @c do
    testEq "coequalizing" "c . f" (cq . f) "c . g" (cq . g)
    Some @z <- genOb @k
    p <- genNamed @(c ~> z) "p"
    let h = p . cq
        factored = Coequalizer.factorCoequalizer cq h
    testEq "factorization" "factored . c" (factored . cq) "h" h
    -- As in 'testEqualizers': an arbitrary arrow that coequalizes must factor, not only one
    -- built by composing through @cq@.
    m <- genNamed @(b ~> z) "m"
    coequalizes <- eqP (m . f) (m . g)
    when coequalizes $
      testEq "existence" "factorCoequalizer c m . c" (Coequalizer.factorCoequalizer cq m . cq) "m" m
    k1 <- genNamed @(c ~> z) "k1"
    k2 <- genNamed @(c ~> z) "k2"
    eqComposed <- eqP (k1 . cq) (k2 . cq)
    propReflectsEq "epi" "k1 . c == k2 . c" eqComposed k1 k2

testCoequalizers_ :: forall k. (Testable k, Coequalizer.HasCoequalizers k, TestObIsOb k) => TestTree
testCoequalizers_ = testCoequalizers @k (\r -> r)

-- | Checks the pullback laws: the pullback cone commutes; it's jointly monic (composing with both
-- legs at once reflects equality); and any compatible cone (built here as @(p1 . j, p2 . j)@ for an
-- arbitrary @j@, so compatibility holds by construction) is correctly recovered by
-- 'Pullback.factorPullback'.
testPullbacks :: forall k. (Testable k, Pullback.HasPullbacks k) => WithTestOb k -> TestTree
testPullbacks withTestOb = testProperty "Pullbacks" $ do
  Some @o <- genOb @k
  Some @a <- genOb @k
  Some @b <- genOb @k
  f <- genNamed @(a ~> o) "f"
  g <- genNamed @(b ~> o) "g"
  Pullback.pullback f g \ @p p1@Objs p2 -> withTestOb @p do
    testEq "commutes" "f . p1" (f . p1) "g . p2" (g . p2)
    Some @z <- genOb @k
    k1' <- genNamed @(z ~> p) "k1"
    k2' <- genNamed @(z ~> p) "k2"
    eq1 <- eqP (p1 . k1') (p1 . k2')
    eq2 <- eqP (p2 . k1') (p2 . k2')
    propReflectsEq "jointly monic" "p1 . k1 == p1 . k2 && p2 . k1 == p2 . k2" (eq1 && eq2) k1' k2'
    j <- genNamed @(z ~> p) "j"
    let k1 = p1 . j
        k2 = p2 . j
        factored = Pullback.factorPullback p1 p2 k1 k2
    testEq "factorization (1)" "p1 . factored" (p1 . factored) "k1" k1
    testEq "factorization (2)" "p2 . factored" (p2 . factored) "k2" k2
    -- And the half those cannot reach: an arbitrary commuting cone must factor too.
    x <- genNamed @(z ~> a) "x"
    y <- genNamed @(z ~> b) "y"
    commutes <- eqP (f . x) (g . y)
    when commutes $ do
      let fac = Pullback.factorPullback p1 p2 x y
      testEq "existence (1)" "p1 . factorPullback p1 p2 x y" (p1 . fac) "x" x
      testEq "existence (2)" "p2 . factorPullback p1 p2 x y" (p2 . fac) "y" y

testPullbacks_ :: forall k. (Testable k, Pullback.HasPullbacks k, TestObIsOb k) => TestTree
testPullbacks_ = testPullbacks @k (\r -> r)

-- | Checks the pushout laws, dual to 'testPullbacks': the pushout cocone commutes; it's jointly epic
-- (post-composing with both legs at once reflects equality); and any compatible cocone (built here as
-- @(j . p1, j . p2)@ for an arbitrary @j@, so compatibility holds by construction) is correctly
-- recovered by 'Pushout.factorPushout'.
testPushouts :: forall k. (Testable k, Pushout.HasPushouts k) => WithTestOb k -> TestTree
testPushouts withTestOb = testProperty "Pushouts" $ do
  Some @o <- genOb @k
  Some @a <- genOb @k
  Some @b <- genOb @k
  f <- genNamed @(o ~> a) "f"
  g <- genNamed @(o ~> b) "g"
  Pushout.pushout f g \ @p p1@Objs p2 -> withTestOb @p do
    testEq "commutes" "p1 . f" (p1 . f) "p2 . g" (p2 . g)
    Some @z <- genOb @k
    k1' <- genNamed @(p ~> z) "k1"
    k2' <- genNamed @(p ~> z) "k2"
    eq1 <- eqP (k1' . p1) (k2' . p1)
    eq2 <- eqP (k1' . p2) (k2' . p2)
    propReflectsEq "jointly epic" "k1 . p1 == k2 . p1 && k1 . p2 == k2 . p2" (eq1 && eq2) k1' k2'
    j <- genNamed @(p ~> z) "j"
    let k1 = j . p1
        k2 = j . p2
        factored = Pushout.factorPushout p1 p2 k1 k2
    testEq "factorization (1)" "factored . p1" (factored . p1) "k1" k1
    testEq "factorization (2)" "factored . p2" (factored . p2) "k2" k2
    -- And the half those cannot reach: an arbitrary commuting cocone must factor too.
    x <- genNamed @(a ~> z) "x"
    y <- genNamed @(b ~> z) "y"
    commutes <- eqP (x . f) (y . g)
    when commutes $ do
      let fac = Pushout.factorPushout p1 p2 x y
      testEq "existence (1)" "factorPushout p1 p2 x y . p1" (fac . p1) "x" x
      testEq "existence (2)" "factorPushout p1 p2 x y . p2" (fac . p2) "y" y

testPushouts_ :: forall k. (Testable k, Pushout.HasPushouts k, TestObIsOb k) => TestTree
testPushouts_ = testPushouts @k (\r -> r)

-- | Checks the epi-mono factorization laws: 'Topos.factorize' splits @f@ as @m . e@ through an
-- image object, with @e@ epi and @m@ mono.
--
-- As with 'testEqualizers' the image object is revealed at runtime rather than computed by a type
-- family, so @withTestOb@ bridges its recovered 'Ob' to 'TestOb'. The epi and mono halves are the
-- two directions of 'propReflectsEq': composing on the right with @e@, and on the left with @m@.
testEpiMonoFactorization
  :: forall k. (Testable k, Topos.HasEpiMonoFactorization k) => WithTestOb k -> TestTree
testEpiMonoFactorization withTestOb = testProperty "Epi-mono factorization" $ do
  Some @a <- genOb @k
  Some @b <- genOb @k
  f <- genNamed @(a ~> b) "f"
  case Topos.factorize f of
    (:.:) @x e@Objs m -> withTestOb @x do
      testEq "factorization" "m . e" (m . e) "f" f
      Some @z <- genOb @k
      k1 <- genNamed @(x ~> z) "k1"
      k2 <- genNamed @(x ~> z) "k2"
      eqEpi <- eqP (k1 . e) (k2 . e)
      propReflectsEq "epi" "k1 . e == k2 . e" eqEpi k1 k2
      j1 <- genNamed @(z ~> x) "k1"
      j2 <- genNamed @(z ~> x) "k2"
      eqMono <- eqP (m . j1) (m . j2)
      propReflectsEq "mono" "m . k1 == m . k2" eqMono j1 j2

testEpiMonoFactorization_
  :: forall k. (Testable k, Topos.HasEpiMonoFactorization k, TestObIsOb k) => TestTree
testEpiMonoFactorization_ = testEpiMonoFactorization @k (\r -> r)

-- * Monoidal structure

-- | The monoidal laws, from @'Proarrow.Tools.Laws.Laws' '['M.Monoidal']@: the unitors and the
-- associator are natural isomorphisms satisfying the triangle and pentagon identities.
testMonoidal :: forall k. (Testable k, M.Monoidal k, TestOb (M.Unit @k)) => WithTestOb2 k -> TestTree
testMonoidal withTestOb2 = testLaws @'[M.Monoidal] "Monoidal" (MonoidalW (\ @a @b r -> withTestOb2 @a @b r) :& WNil)

testMonoidal_ :: forall k. (Testable k, M.Monoidal k, TestObIsOb k) => TestTree
testMonoidal_ = testMonoidal @k (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The laws of a symmetric monoidal category, from
-- @'Proarrow.Tools.Laws.Laws' 'M.SymMonoidalStructures'@: 'M.swap' is a natural
-- self-inverse satisfying the hexagon identity.
testSymMonoidal :: forall k. (Testable k, M.SymMonoidal k, TestOb (M.Unit @k)) => WithTestOb2 k -> TestTree
testSymMonoidal withTestOb2 =
  testLaws @M.SymMonoidalStructures
    "Symmetric monoidal"
    (MonoidalW (\ @a @b r -> withTestOb2 @a @b r) :& SymMonoidalW :& WNil)

testSymMonoidal_ :: forall k. (Testable k, M.SymMonoidal k, TestObIsOb k) => TestTree
testSymMonoidal_ = testSymMonoidal @k (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The laws of a copy-discard category: every object is a cocommutative comonoid (the laws of its
-- supply, in "Proarrow.Monoid"), and 'CopyDiscard.copy' and 'CopyDiscard.discard' are that comonoid
-- and respect the tensor ('CopyDiscard.CopyDiscardStructures').
testCopyDiscard
  :: forall k. (Testable k, CopyDiscard.CopyDiscard k, TestOb (M.Unit @k)) => WithTestOb2 k -> TestTree
testCopyDiscard withTestOb2 =
  testGroup
    "CopyDiscard"
    [ testLaws @'[M.Monoidal, Monoid.Supplies Monoid.Comonoid] "Comonoids" (monoidal :& ComonoidSupplyW :& WNil)
    , testLaws @'[M.Monoidal, M.SymMonoidal, Monoid.Supplies Monoid.CocommutativeComonoid]
        "Cocommutative comonoids"
        (monoidal :& SymMonoidalW :& CocommutativeComonoidSupplyW :& WNil)
    , testLaws @CopyDiscard.CopyDiscardStructures "Copy and discard" (monoidal :& SymMonoidalW :& CopyDiscardW :& WNil)
    ]
  where
    monoidal = MonoidalW (\ @a @b r -> withTestOb2 @a @b r)

-- | 'testCopyDiscard' where 'TestOb' is 'Ob'. 'Ob' goes through 'obFromTestOb', because with the
-- comonoid supply in scope GHC does not find the @TestOb a => Ob' a => Ob a@ route on its own.
testCopyDiscard_ :: forall k. (Testable k, CopyDiscard.CopyDiscard k, TestObIsOb k) => TestTree
testCopyDiscard_ = testCopyDiscard @k (\ @a @b r -> obFromTestOb @a (obFromTestOb @b (M.withOb2 @k @a @b r)))

-- | The coherence law tying 'Cartesian.Cartesian' to its 'CopyDiscard.CopyDiscard' superclass
-- (Fox's theorem): the comonoid supplied on every object is the natural one, @copy = id &&& id@
-- and @discard = terminate@.
testCartesian
  :: forall k
   . (Testable k, Cartesian.Cartesian k, TestOb (M.Unit @k))
  => (forall (a :: k) r. (TestOb a) => ((Ob a) => r) -> r)
  -> WithTestOb2 k
  -> TestTree
testCartesian withOb withTestOb2 = testProperty "Cartesian" $ do
  Some @a <- genOb @k
  withOb @a (withTestOb2 @a @a (propCartesianAt @a))

-- Hoisted so that @a ** a ~ a && a@ is an ordinary given ('Cartesian.TensorIsProduct'), which the
-- quantified superclass of 'Cartesian.Cartesian' can't supply as a rewrite on its own.
propCartesianAt
  :: forall {k} (a :: k)
   . ( Testable k
     , Cartesian.Cartesian k
     , Cartesian.TensorIsProduct a a
     , TestOb (M.Unit @k)
     , TestOb a
     , Ob a
     , TestOb (a M.** a)
     )
  => Property ()
propCartesianAt = do
  testEq "copy" "copy" (CopyDiscard.copy @k @a) "id &&& id" (BinaryProduct.diag @a)
  testEq "discard" "discard" (CopyDiscard.discard @k @a) "terminate" (Terminal.terminate @k @a)

testCartesian_ :: forall k. (Testable k, Cartesian.Cartesian k, TestObIsOb k, TestOb (M.Unit @k)) => TestTree
testCartesian_ =
  testCartesian @k (\ @a r -> obFromTestOb @a r) (\ @a @b r -> obFromTestOb @a (obFromTestOb @b (M.withOb2 @k @a @b r)))

-- | The tensor distributes over coproducts and is absorbed by the initial object, from
-- @'Proarrow.Tools.Laws.Laws' 'Distributive.DistributiveStructures'@: 'Distributive.distL',
-- 'Distributive.distR', 'Distributive.absorbL' and 'Distributive.absorbR' are isomorphisms, with
-- the inverses 'Distributive.distLInv', 'Distributive.distRInv' and 'Initial.initiate'.
testDistributive
  :: forall k
   . (Testable k, Distributive.Distributive k, TestOb (M.Unit @k), TestOb (Initial.InitialObject :: k))
  => WithTestOb2 k
  -> WithTestObCoprod k
  -> TestTree
testDistributive withTestOb2 withTestObCoprod =
  testLaws @Distributive.DistributiveStructures
    "Distributive"
    ( MonoidalW (\ @a @b r -> withTestOb2 @a @b r)
        :& InitialW
        :& CoproductsW (\ @a @b r -> withTestObCoprod @a @b r)
        :& DistributiveW
        :& WNil
    )

testDistributive_ :: forall k. (Testable k, Distributive.Distributive k, TestObIsOb k) => TestTree
testDistributive_ =
  testDistributive @k
    (\ @a @b r -> M.withOb2 @k @a @b r)
    (\ @a @b r -> BinaryCoproduct.withObCoprod @k @a @b r)

-- | The laws of a closed monoidal category, from
-- @'Proarrow.Tools.Laws.Laws' 'Exponential.ClosedStructures'@: 'Exponential.apply' undoes
-- 'Exponential.curry' and every arrow into an exponential is the 'Exponential.curry' of one,
-- 'Exponential.curry' is natural, and 'Exponential.^^^' is defined from 'Exponential.curry' and
-- 'Exponential.apply'.
testClosed
  :: forall k
   . (Testable k, Exponential.Closed k, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> WithTestObExp k
  -> TestTree
testClosed withTestOb2 withTestObExp =
  testLawsWith @Exponential.ClosedStructures
    (genObSmall @k)
    "Closed"
    (MonoidalW (\ @a @b r -> withTestOb2 @a @b r) :& ClosedW (\ @a @b r -> withTestObExp @a @b r) :& WNil)

testClosed_ :: forall k. (Testable k, Exponential.Closed k, TestObIsOb k) => TestTree
testClosed_ =
  testClosed @k
    (\ @a @b r -> M.withOb2 @k @a @b r)
    (\ @a @b r -> Exponential.withObExp @k @a @b r)

-- | Laws of a *-autonomous category, from
-- @'Proarrow.Tools.Laws.Laws' 'SA.StarAutonomousStructures'@:
-- 'SA.dual' is a contravariant functor, bijective on hom-sets with inverse 'SA.dualInv';
-- 'SA.doubleNeg' is an isomorphism; and 'SA.linDist' is a natural bijection
-- @Hom(a ** b, Dual c) ≅ Hom(a, Dual (b ** c))@ with inverse 'SA.linDistInv'. The exponential
-- witness is needed because 'Exponential.Closed' is a superclass, although no law builds an
-- exponential.
testStarAutonomous
  :: forall k
   . (Testable k, SA.StarAutonomous k, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> WithTestObExp k
  -> WithTestObDual k
  -> TestTree
testStarAutonomous withTestOb2 withTestObExp withTestObDual =
  testLaws @SA.StarAutonomousStructures
    "*-autonomous"
    ( MonoidalW (\ @a @b r -> withTestOb2 @a @b r)
        :& SymMonoidalW
        :& ClosedW (\ @a @b r -> withTestObExp @a @b r)
        :& StarAutonomousW (\ @a r -> withTestObDual @a r)
        :& WNil
    )

testStarAutonomous_ :: forall k. (Testable k, SA.StarAutonomous k, TestObIsOb k) => TestTree
testStarAutonomous_ =
  testStarAutonomous @k
    (\ @a @b r -> M.withOb2 @k @a @b r)
    (\ @a @b r -> Exponential.withObExp @k @a @b r)
    (\ @a r -> r \\ SA.dualObj @a)

-- | Laws of a compact closed category, from
-- @'Proarrow.Tools.Laws.Laws' 'CC.CompactClosedStructures'@:
-- 'CC.distribDual' and 'CC.dualUnit' are isomorphisms (so 'SA.Dual' is strong monoidal), and
-- 'CC.dualityUnit' and 'CC.dualityCounit' satisfy the zigzag identities. See
-- 'testStarAutonomous' for the exponential witness.
testCompactClosed
  :: forall k
   . (Testable k, CC.CompactClosed k, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> WithTestObExp k
  -> WithTestObDual k
  -> TestTree
testCompactClosed withTestOb2 withTestObExp withTestObDual =
  testLaws @CC.CompactClosedStructures
    "Compact closed"
    ( MonoidalW (\ @a @b r -> withTestOb2 @a @b r)
        :& SymMonoidalW
        :& ClosedW (\ @a @b r -> withTestObExp @a @b r)
        :& StarAutonomousW (\ @a r -> withTestObDual @a r)
        :& CompactClosedW
        :& WNil
    )

testCompactClosed_ :: forall k. (Testable k, CC.CompactClosed k, TestObIsOb k) => TestTree
testCompactClosed_ =
  testCompactClosed @k
    (\ @a @b r -> M.withOb2 @k @a @b r)
    (\ @a @b r -> Exponential.withObExp @k @a @b r)
    (\ @a r -> r \\ SA.dualObj @a)

-- | The laws of a category that supplies special commutative Frobenius algebras, stated for
-- every object: the monoid and comonoid laws of its points, their commutativity, and the Frobenius laws of
-- 'Hypergraph.FrobeniusStructures'.
testHypergraph
  :: forall k
   . ( Testable k
     , M.SymMonoidal k
     , Monoid.Supplies Monoid.CommutativeMonoid k
     , Monoid.Supplies Monoid.CocommutativeComonoid k
     , TestOb (M.Unit @k)
     )
  => WithTestOb2 k
  -> TestTree
testHypergraph withTestOb2 =
  testGroup
    "Hypergraph (Frobenius supply)"
    [ testLaws @'[M.Monoidal, Monoid.Supplies Monoid.Monoid] "Monoids" (monoidal :& MonoidSupplyW :& WNil)
    , testLaws @'[M.Monoidal, Monoid.Supplies Monoid.Comonoid] "Comonoids" (monoidal :& ComonoidSupplyW :& WNil)
    , testLaws @'[M.Monoidal, M.SymMonoidal, Monoid.Supplies Monoid.CommutativeMonoid]
        "Commutative monoids"
        (monoidal :& SymMonoidalW :& CommutativeMonoidSupplyW :& WNil)
    , testLaws @'[M.Monoidal, M.SymMonoidal, Monoid.Supplies Monoid.CocommutativeComonoid]
        "Cocommutative comonoids"
        (monoidal :& SymMonoidalW :& CocommutativeComonoidSupplyW :& WNil)
    , testLaws @Hypergraph.FrobeniusStructures
        "Frobenius"
        (monoidal :& SymMonoidalW :& MonoidSupplyW :& ComonoidSupplyW :& WNil)
    ]
  where
    monoidal = MonoidalW (\ @a @b r -> withTestOb2 @a @b r)

testHypergraph_
  :: forall k
   . ( Testable k
     , M.SymMonoidal k
     , TestObIsOb k
     , Monoid.Supplies Monoid.CommutativeMonoid k
     , Monoid.Supplies Monoid.CocommutativeComonoid k
     )
  => TestTree
testHypergraph_ = testHypergraph @k (\ @a @b r -> obFromTestOb @a (obFromTestOb @b (M.withOb2 @k @a @b r)))

-- * Traced monoidal categories

-- | The trace laws of "Proarrow.Category.Monoidal.Strength" ('Strength.TracedStructures').
testTraced
  :: forall k. (Testable k, Strength.TracedMonoidal k, TestOb (M.Unit @k)) => WithTestOb2 k -> TestTree
testTraced withTestOb2 =
  -- small objects: the laws tensor up to three of them together, and a relation or matrix between
  -- such tensors grows with the product of their sizes
  testLawsWith @Strength.TracedStructures
    (genObSmall @k)
    "Traced"
    (MonoidalW (\ @a @b r -> withTestOb2 @a @b r) :& SymMonoidalW :& TracedW :& WNil)

testTraced_ :: forall k. (Testable k, Strength.TracedMonoidal k, TestObIsOb k) => TestTree
testTraced_ = testTraced @k (\ @a @b r -> M.withOb2 @k @a @b r)

-- * Monoids and comonoids

-- | The monoid laws of @m@: 'Monoid.mempty' is a left and right unit for 'Monoid.mappend' (up to the
-- unitors), and 'Monoid.mappend' is associative (up to the associator).
propMonoid
  :: forall {k} m
   . (Testable k, Monoid.Monoid (m :: k), TestOb m, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> Property ()
propMonoid withTestOb2 =
  withTestOb2 @M.Unit @m $
    withTestOb2 @m @M.Unit $ do
      testEq
        "left identity"
        "μ . (η ⊗ 1)"
        (Monoid.mappend . (Monoid.mempty @m M.** obj @m))
        "λ"
        (M.leftUnitor @k @m)
      testEq
        "right identity"
        "μ . (1 ⊗ η)"
        (Monoid.mappend . (obj @m M.** Monoid.mempty @m))
        "ρ"
        (M.rightUnitor @k @m)
      withTestOb2 @m @m $ withTestOb2 @(m M.** m) @m $ do
        testEq
          "associativity"
          "μ . (μ ⊗ 1)"
          (Monoid.mappend @m . (Monoid.mappend @m M.** obj @m))
          "μ . (1 ⊗ μ) . α"
          (Monoid.mappend . (obj @m M.** Monoid.mappend @m) . M.associator @k @m @m @m)

-- | The laws of a commutative monoid: 'propMonoid', and 'Monoid.mappend' is unchanged by 'M.swap'.
propCommutativeMonoid
  :: forall {k} m
   . (Testable k, Monoid.CommutativeMonoid (m :: k), TestOb m, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> Property ()
propCommutativeMonoid withTestOb2 = do
  propMonoid @m (\ @x @y r -> withTestOb2 @x @y r)
  withTestOb2 @m @m $
    testEq
      "commutativity"
      "mappend . swap"
      (Monoid.mappend @m . M.swap @k @m @m)
      "mappend"
      (Monoid.mappend @m)

-- | The laws of a cocommutative comonoid, as those of a commutative monoid in the opposite category.
propCocommutativeComonoid
  :: forall {k} m
   . (Testable k, Monoid.CocommutativeComonoid (m :: k), TestOb m, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> Property ()
propCocommutativeComonoid withTestOb2 = do
  propCommutativeMonoid @(OP m) (\ @(OP x) @(OP y) r -> withTestOb2 @x @y r)

-- | Check that the object @m@ is a special commutative 'Hypergraph.Frobenius' algebra: it is a
-- 'Monoid.CommutativeMonoid' (via 'propCommutativeMonoid') and a 'Monoid.CocommutativeComonoid'
-- (via 'propCocommutativeComonoid'), and satisfies speciality (@mappend . comult = id@) and the
-- Frobenius condition. A 'Hypergraph.Hypergraph' category supplies this structure, and
-- @'testHypergraph'@ samples an object and delegates here.
propFrobenius
  :: forall {k} m
   . ( Testable k
     , M.SymMonoidal k
     , Monoid.CommutativeMonoid (m :: k)
     , Monoid.CocommutativeComonoid m
     , TestOb m
     , TestOb (M.Unit @k)
     )
  => WithTestOb2 k
  -> Property ()
propFrobenius withTestOb2 = do
  propCommutativeMonoid @m (\ @x @y r -> withTestOb2 @x @y r)
  propCocommutativeComonoid @m (\ @x @y r -> withTestOb2 @x @y r)
  withTestOb2 @m @m $
    withTestOb2 @(m M.** m) @m $ do
      let mu = Monoid.mappend @m
          delta = Monoid.comult @m
      testEq
        "speciality"
        "mappend . comult"
        (mu . delta)
        "id"
        (obj @m)
      testEq
        "Frobenius condition (left)"
        "(mappend ** id) . associatorInv . (id ** comult)"
        ((mu M.** obj @m) . M.associatorInv @k @m @m @m . (obj @m M.** delta))
        "comult . mappend"
        (delta . mu)
      testEq
        "Frobenius condition (right)"
        "(id ** mappend) . associator . (comult ** id)"
        ((obj @m M.** mu) . M.associator @k @m @m @m . (delta M.** obj @m))
        "comult . mappend"
        (delta . mu)

-- | The monoid laws of @m@ ('propMonoid') as a ready-made test.
testMonoid
  :: forall {k} m
   . (Testable k, Monoid.Monoid (m :: k), TestOb m, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> TestTree
testMonoid f = testProperty ("Monoid " ++ showOb @k @m) (propMonoid @m \ @a @b -> f @a @b)

testMonoid_ :: forall {k} m. (Testable k, Monoid.Monoid (m :: k), TestObIsOb k) => TestTree
testMonoid_ = testMonoid @m (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The comonoid laws of @m@, as the monoid laws of @m@ in the opposite category.
testComonoid
  :: forall {k} m
   . (Testable k, Monoid.Comonoid (m :: k), TestOb m, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> TestTree
testComonoid f = testProperty ("Comonoid " ++ showOb @k @m) (propMonoid @(OP m) \ @(OP a) @(OP b) r -> f @a @b r)

testComonoid_ :: forall {k} m. (Testable k, Monoid.Comonoid (m :: k), TestObIsOb k) => TestTree
testComonoid_ = testComonoid @m (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The laws of a commutative monoid ('propCommutativeMonoid') as a ready-made test.
testCommutativeMonoid
  :: forall {k} m
   . (Testable k, Monoid.CommutativeMonoid (m :: k), TestOb m, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> TestTree
testCommutativeMonoid f = testProperty ("CommutativeMonoid " ++ showOb @k @m) (propCommutativeMonoid @m \ @a @b -> f @a @b)

testCommutativeMonoid_ :: forall {k} m. (Testable k, Monoid.CommutativeMonoid (m :: k), TestObIsOb k) => TestTree
testCommutativeMonoid_ = testCommutativeMonoid @m (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The laws of a cocommutative comonoid ('propCocommutativeComonoid') as a ready-made test.
testCocommutativeComonoid
  :: forall {k} m
   . (Testable k, Monoid.CocommutativeComonoid (m :: k), TestOb m, TestOb (M.Unit @k))
  => WithTestOb2 k
  -> TestTree
testCocommutativeComonoid f = testProperty ("CocommutativeComonoid " ++ showOb @k @m) (propCocommutativeComonoid @m \ @a @b -> f @a @b)

testCocommutativeComonoid_
  :: forall {k} m
   . (Testable k, Monoid.CocommutativeComonoid (m :: k), TestObIsOb k)
  => TestTree
testCocommutativeComonoid_ = testCocommutativeComonoid @m (\ @a @b r -> M.withOb2 @k @a @b r)

-- | The laws of a special commutative Frobenius algebra ('propFrobenius') as a ready-made test.
testFrobenius
  :: forall {k} (m :: k)
   . ( Testable k
     , Monoid.CommutativeMonoid m
     , Monoid.CocommutativeComonoid m
     , TestOb m
     , TestOb (M.Unit @k)
     )
  => WithTestOb2 k
  -> TestTree
testFrobenius f = testProperty ("Frobenius " ++ showOb @k @m) (propFrobenius @m \ @a @b -> f @a @b)

testFrobenius_
  :: forall {k} (m :: k)
   . ( Testable k
     , Monoid.CommutativeMonoid m
     , Monoid.CocommutativeComonoid m
     , TestObIsOb k
     )
  => TestTree
testFrobenius_ = testFrobenius @m (\ @a @b r -> M.withOb2 @k @a @b r)

-- * Toposes

-- | Checks the subobject classifier @'Topos.Omega'@, the object of truth values: a map into it is a
-- predicate, and each mono @m@ has one classifying map, 'Topos.true' on @m@ and nowhere else. The
-- laws quantify over generalized elements (arrows into the object):
--
-- * @'Topos.classifyGraph' f@ is 'Topos.true' at @(x, y)@ iff @y = f . x@. This is the pullback
--   condition for the graph @\<id, f\>@. At @f = 'id'@ it is 'Topos.isEq', so equality testing is
--   pinned down too.
-- * Distinct arrows get distinct classifiers, the testable consequence of the classifying map
--   being unique.
-- * @'Topos.classifyKernelPair' f@ is true at @(x, x\')@ iff @f@ identifies them, in both directions.
-- * @'Topos.classifyImage' f@ is true on the image of @f@ and nowhere else: every element it calls
--   true factors through the image mono, by 'Pullback.factorPullback'. This is the law that
--   separates a subobject classifier from an arbitrary map into 'Topos.Omega'.
--
-- The elements come from the 'Testable' palette, which is sound iff that palette generates: true
-- for the concrete finite categories, not for a presheaf topos.
testSubobjectClassifier
  :: forall k
   . ( Testable k
     , Topos.HasSubobjectClassifier k
     , Topos.HasEpiMonoFactorization k
     , Pushout.HasPushouts k
     , Pullback.HasPullbacks k
     , TestOb (Topos.Omega :: k)
     )
  => WithTestObProd k
  -> TestTree
testSubobjectClassifier withTestObProd = testProperty "Subobject classifier" $ do
  Some @a <- genOb @k
  Some @b <- genOb @k
  Some @z <- genOb @k
  f <- genNamed @(a ~> b) "f"
  x <- genNamed @(z ~> a) "x"
  y <- genNamed @(z ~> b) "y"
  inGraph <- eqP (f . x) y
  classified <-
    eqP (Topos.classifyGraph f . (x BinaryProduct.&&& y)) (Terminal.const Topos.true)
  expect "classifyGraph is true exactly on the graph of f" inGraph classified
  g <- genNamed @(a ~> b) "g"
  withTestObProd @a @b @(Property ()) do
    eqChi <- eqP (Topos.classifyGraph f) (Topos.classifyGraph g)
    propReflectsEq "classifier injective" "classifyGraph f == classifyGraph g" eqChi f g
  x' <- genNamed @(z ~> a) "x'"
  identified <- eqP (f . x) (f . x')
  kernelPair <-
    eqP (Topos.classifyKernelPair f . (x BinaryProduct.&&& x')) (Terminal.const Topos.true)
  expect "classifyKernelPair is true exactly when f identifies the pair" identified kernelPair
  -- bound once: in the sheaves this is a pushout, which sheafifies and tabulates its apex
  let chi = Topos.classifyImage f
  onImage <- eqP (chi . (f . x)) (Terminal.const Topos.true)
  expect "classifyImage f is true on the image of f" True onImage
  -- The converse, which makes this a /subobject/ classifier: anything the classifier calls true
  -- factors through the image mono. Mirrors the existence half of 'testEqualizers'.
  case Topos.factorize f of
    (:.:) _ m@Objs -> do
      w <- genNamed @(z ~> b) "w"
      classifiedTrue <- eqP (chi . w) (Terminal.const Topos.true)
      when classifiedTrue $
        testEq
          "image factorization"
          "m . factorPullback m terminate w terminate"
          (m . Pullback.factorPullback m Terminal.terminate w Terminal.terminate)
          "w"
          w

testSubobjectClassifier_
  :: forall k
   . ( Testable k
     , Topos.HasSubobjectClassifier k
     , Topos.HasEpiMonoFactorization k
     , Pushout.HasPushouts k
     , Pullback.HasPullbacks k
     , TestObIsOb k
     , TestOb (Topos.Omega :: k)
     )
  => TestTree
testSubobjectClassifier_ =
  testSubobjectClassifier @k (\ @a @b r -> BinaryProduct.withObProd @k @a @b r)

-- | Negation is implication into false:
-- @'Topos.not' = 'Topos.implies' . (id '&&&' 'Terminal.const' 'Topos.false')@. A theorem of
-- every topos; 'Topos.not' is defined as the classifying map of 'Topos.false' instead, so this
-- checks the two agree.
testNegation :: forall k. (Testable k, Topos.ElementaryTopos k, TestOb (Topos.Omega :: k)) => TestTree
testNegation =
  testProperty "negation is implication into false" $
    testEq
      "not"
      "not"
      (Topos.not @k)
      "implies . (id &&& const false)"
      (Topos.implies . (id BinaryProduct.&&& Terminal.const Topos.false))

-- | The three equations a Lawvere–Tierney topology satisfies, for an arrow
-- @j :: 'Topos.Omega' '~>' 'Topos.Omega'@: it fixes @true@, is idempotent, and preserves meets.
--
-- Such a @j@ is the same data as a Grothendieck topology: the covering sieves are the ones @j@
-- sends to @true@. 'FinSheaf.lawvereTierney' is the @j@ a coverage induces, so this is how a
-- coverage's stability and composition get checked, without quantifying over arrows the coverage
-- was never handed.
testLawvereTierney
  :: forall k
   . (Testable k, Topos.ElementaryTopos k, TestOb (Topos.Omega :: k), TestOb (Terminal.TerminalObject :: k))
  => WithTestObProd k
  -> (Topos.Omega :: k) ~> Topos.Omega
  -> TestTree
testLawvereTierney withTestObProd j =
  testGroup
    "Lawvere-Tierney topology"
    [testProperty name (law j) | (name, law) <- lawvereTierneyLaws @k (\ @a @b r -> withTestObProd @a @b r)]

-- | The three laws of 'testLawvereTierney', by name, for an arrow given later. Shared by it and
-- 'testLawvereTierneyFamily'.
lawvereTierneyLaws
  :: forall k
   . (Testable k, Topos.ElementaryTopos k, TestOb (Topos.Omega :: k), TestOb (Terminal.TerminalObject :: k))
  => WithTestObProd k
  -> [(String, (Topos.Omega :: k) ~> Topos.Omega -> Property ())]
lawvereTierneyLaws withTestObProd =
  [ ("fixes true", \j -> testEq "true" "j . true" (j . Topos.true) "true" Topos.true)
  , ("idempotent", \j -> testEq "idempotent" "j . j" (j . j) "j" j)
  ,
    ( "preserves meets"
    , \j ->
        withTestObProd @Topos.Omega @Topos.Omega @(Property ()) $
          testEq "meets" "j . and" (j . Topos.and) "and . (j *** j)" (Topos.and . (j BinaryProduct.*** j))
    )
  ]

-- | 'testLawvereTierney' for a family of arrows indexed by the truth values, at a generated one:
-- 'Topos.openTopology' and 'Topos.closedTopology' are the ones the internal logic gives.
testLawvereTierneyFamily
  :: forall k
   . (Testable k, Topos.ElementaryTopos k, TestOb (Topos.Omega :: k), TestOb (Terminal.TerminalObject :: k))
  => String
  -> WithTestObProd k
  -> ((Terminal.TerminalObject :: k) ~> Topos.Omega -> (Topos.Omega :: k) ~> Topos.Omega)
  -> TestTree
testLawvereTierneyFamily name withTestObProd family =
  testGroup
    name
    [ testProperty lawName (genNamed "u" >>= law . family)
    | (lawName, law) <- lawvereTierneyLaws @k (\ @a @b r -> withTestObProd @a @b r)
    ]

testLawvereTierney_
  :: forall k
   . ( Testable k
     , Topos.ElementaryTopos k
     , TestObIsOb k
     , TestOb (Topos.Omega :: k)
     , TestOb (Terminal.TerminalObject :: k)
     )
  => (Topos.Omega :: k) ~> Topos.Omega
  -> TestTree
testLawvereTierney_ =
  testLawvereTierney @k (\ @a @b r -> obFromTestOb @a (obFromTestOb @b (BinaryProduct.withObProd @k @a @b r)))

testLawvereTierneyFamily_
  :: forall k
   . ( Testable k
     , Topos.ElementaryTopos k
     , TestObIsOb k
     , TestOb (Topos.Omega :: k)
     , TestOb (Terminal.TerminalObject :: k)
     )
  => String
  -> ((Terminal.TerminalObject :: k) ~> Topos.Omega -> (Topos.Omega :: k) ~> Topos.Omega)
  -> TestTree
testLawvereTierneyFamily_ name =
  testLawvereTierneyFamily @k name (\ @a @b r -> obFromTestOb @a (obFromTestOb @b (BinaryProduct.withObProd @k @a @b r)))

-- * Sites and sheaves

-- | The three laws a listable, stable coverage owes, as one group: 'testStableSite',
-- 'testGeneratedSieveIsSieve' and 'testDenseIsCovering'. Checking a site starts here.
--
-- The rest of this section checks what is built on the coverage: 'testGluesBack' and
-- 'testGluesBackAt' the sheaf condition, 'testEqualizersAreSheaves' the category of sheaves, and
-- 'testSheafification' (with 'testPlusFixes') the reflector. 'testLawvereTierney', under Toposes,
-- checks the generated topology and with it 'Sheaf.HasFiniteCovers'\'s Composition law.
testSiteLaws
  :: forall t j k
   . (Sheaf.StableSite t k, Sheaf.HasFiniteCovers t k, Finitary.FiniteCat j, Finitary.FiniteCat k)
  => TestTree
testSiteLaws =
  testGroup
    "site laws"
    [testStableSite @t @k, testGeneratedSieveIsSieve @t @j @k, testDenseIsCovering @t @j @k]

-- | 'Sheaf.StableSite'\'s law: pulling a cover back along @f@ gives a cover. For every leg @l'@ of
-- the cover 'Sheaf.pullbackCover' returns, the instance names a leg @l@ of the original and a
-- factor @u@ with @f '.' 'Sheaf.legArrow' l' = 'Sheaf.legArrow' l '.' u@. This checks that
-- equation, and that the pulled-back legs generate a covering sieve, which matters for covers that
-- are built instead of listed, like the meets of 'Proarrow.Category.Sheaf.Joins'. Covering is
-- checked instead of dense so that the check does not assume covers compose. It runs at
-- @j ~ ()@, since whether a sieve covers does not depend on @j@.
--
-- On a thin site (at most one arrow between two objects) the equation holds as soon as it
-- typechecks. It bites only where two legs share a source and target: the ends of an edge in the
-- graph schema @E ⇉ V@, or 'Proarrow.Category.Sheaf.ByImage', whose legs are named by elements.
testStableSite
  :: forall t k. (Sheaf.StableSite t k, Sheaf.HasFiniteCovers t k, Finitary.FiniteCat k) => TestTree
testStableSite =
  testProperty "covers pull back" $
    sequence_
      ( Finitary.foreachOb @k @(Property ()) \ @a -> Finitary.foreachOb @k @(Property ()) \ @b ->
          [ case Sheaf.pullbackCover c f of
              -- the arrow itself factors, so the pullback is @b@\'s implicit identity cover
              Sheaf.AlreadyFactors fs -> propFactorsThroughLeg @t f fs
              Sheaf.PulledBack c' fs -> do
                expect
                  ("the pulled-back cover of object " ++ show (Finitary.objIndex @b) ++ " covers it")
                  True
                  (FinSheaf.isCovering @t (FinSheaf.generatedSieve @t @b @'() c'))
                sequence_ [propFactorsThroughLeg @t (f . Sheaf.legArrow l) (fs l) | Sheaf.SomeLeg l <- Sheaf.legs c']
          | Sheaf.SomeCover c <- Sheaf.covers @t @k @a
          , f <- Finitary.elements @(Hom k) @b @a
          ]
      )

-- | One leg\'s half of 'testStableSite': the arrow is the leg it factors through, composed with
-- the factor.
propFactorsThroughLeg
  :: forall t {k} (a :: k) c x
   . (Sheaf.Site t k, Finitary.FiniteCat k, Ob a)
  => x ~> a
  -> Sheaf.Factors t k a c x
  -> Property ()
propFactorsThroughLeg f (Sheaf.Factors l u) =
  -- the factor is what brings its own source into scope, and the result type is known here, where
  -- at the call site it would be under an untouchable variable
  u //
    expect
      "the arrow factors through the leg the pullback names"
      (Finitary.toIndex @(Hom k) @x @a f)
      (Finitary.toIndex (Sheaf.legArrow l . u))

-- | A cover's generated sieve (the arrows that factor through one of its legs) is a sieve: closed
-- under composing on either side, as 'FinTopos.closedUnder' decides. That holds for any coverage,
-- lawful or not, so this checks 'Finitary.factorsThrough' and the hom-profunctor's
-- 'Finitary.elements', which every verdict in "Proarrow.Category.Enriched.Finitary.Sheaf" is read
-- off. It catches, for instance, an argument-swapped 'Finitary.factorsThrough'.
testGeneratedSieveIsSieve
  :: forall t j k. (Sheaf.HasFiniteCovers t k, Finitary.FiniteCat j, Finitary.FiniteCat k) => TestTree
testGeneratedSieveIsSieve =
  testProperty "generated sieves are sieves" $
    sequence_
      ( Finitary.foreachOb @k @(Property ()) \ @a -> Finitary.foreachOb @j @(Property ()) \ @b ->
          [ case FinSheaf.generatedSieve @t @a @b c of
              Sieve inSieve ->
                expect
                  ("the sieve a cover of object " ++ show (Finitary.objIndex @a) ++ " generates")
                  True
                  (FinTopos.closedUnder @(Yo a (OP b)) \(Yo g h) -> inSieve g h)
          | Sheaf.SomeCover c <- Sheaf.covers @t @k @a
          ]
      )

-- | On a category with pullbacks (which give the Ore condition), the topology 'Sheaf.Atomic'
-- generates is the double-negation one: 'FinSheaf.lawvereTierney' and 'Topos.doubleNegation' are
-- the same arrow, one computed by closing sieves and one from the internal logic. At @j ~ ()@
-- only: over a non-trivial @j@, @¬¬@ is the dense topology along @j@ as well, while the coverage
-- acts on @k@ alone.
testAtomicIsDoubleNegation
  :: forall k
   . ( Pullback.HasPullbacks k
     , Finitary.FiniteCat k
     , Testable (BinaryProduct.PROD (FinTopos.FINITARY () k))
     , TestOb (Topos.Omega :: BinaryProduct.PROD (FinTopos.FINITARY () k))
     )
  => TestTree
testAtomicIsDoubleNegation =
  testProperty "double negation is the atomic topology" $
    testEq
      "¬¬"
      "doubleNegation"
      (Topos.doubleNegation @(BinaryProduct.PROD (FinTopos.FINITARY () k)))
      "lawvereTierney @Atomic"
      (FinSheaf.lawvereTierney @Sheaf.Atomic)

-- | A profunctor @p :: j +-> k@ is fully faithful in the sense of 'Ran' when the functor from @k@ to
-- copresheaves on @j@ sending @a@ to @p a (-)@ is: each hom-set @a ~> a'@ is in bijection with the
-- natural transformations @p a' (-) -> p a (-)@, which are @(p '|>' p) a a'@. For a corepresentable
-- @p@ that is the functor @p '%%' -@ being fully faithful, which is
-- 'Proarrow.Category.Sheaf.ByImage'\'s Fully faithful law. 'testRiftFullyFaithful' is the other
-- side, and neither implies the other.
testRanFullyFaithful
  :: forall {j} {k} (p :: j +-> k). (Finitary.Finitary p, Finitary.FiniteCat j, Finitary.FiniteCat k) => TestTree
testRanFullyFaithful =
  testProperty "fully faithful into copresheaves" $
    sequence_
      ( Finitary.foreachOb @k @(Property ()) \ @a -> Finitary.foreachOb @k @(Property ()) \ @a' ->
          let ix = Finitary.toIndex @(Ran (OP p) p) @a @a'
          in [ expect
                 ( "each arrow from object "
                     ++ show (Finitary.objIndex @a)
                     ++ " to object "
                     ++ show (Finitary.objIndex @a')
                     ++ " is one transformation"
                 )
                 (Finitary.indices (Finitary.size @(Ran (OP p) p) @a @a'))
                 (sort [ix (Ran (lmap f)) | f <- Finitary.elements @(Hom k) @a @a'])
             ]
      )

-- | A profunctor @p :: j +-> k@ is fully faithful in the sense of 'Rift' when the functor from @j@ to
-- presheaves on @k@ sending @b@ to @p (-) b@ is: each hom-set @b ~> b'@ is in bijection with the
-- natural transformations @p (-) b -> p (-) b'@, which are @(p '<|' p) b b'@. For a representable
-- @p@ that is the functor @p '%' -@ being fully faithful. For a corepresentable @p@ it says the
-- image of @p '%%' -@ is dense. That makes 'Proarrow.Category.Sheaf.ByImage' subcanonical, and
-- when @p '%%' -@ is fully faithful the converse holds too. The comparison lemma asks for something else, 'testCoveredByImage'.
testRiftFullyFaithful
  :: forall {j} {k} (p :: j +-> k). (Finitary.Finitary p, Finitary.FiniteCat j, Finitary.FiniteCat k) => TestTree
testRiftFullyFaithful =
  testProperty "fully faithful into presheaves" $
    sequence_
      ( Finitary.foreachOb @j @(Property ()) \ @b -> Finitary.foreachOb @j @(Property ()) \ @b' ->
          let ix = Finitary.toIndex @(Rift (OP p) p) @b @b'
          in [ expect
                 ( "each arrow from object "
                     ++ show (Finitary.objIndex @b)
                     ++ " to object "
                     ++ show (Finitary.objIndex @b')
                     ++ " is one transformation"
                 )
                 (Finitary.indices (Finitary.size @(Rift (OP p) p) @b @b'))
                 (sort [ix (Rift (rmap f)) | f <- Finitary.elements @(Hom j) @b @b'])
             ]
      )

-- | Every object of @j@ is covered, for the coverage @t@, by the arrows into it from the image of
-- the functor @w '%%' -@: the sieve they generate is covering. With @w '%%' -@ fully faithful
-- ('testRanFullyFaithful') this is the hypothesis of the comparison lemma, see
-- 'Proarrow.Category.Sheaf.Induced'. Neither this nor 'testRiftFullyFaithful' implies the other.
testCoveredByImage
  :: forall t {j} {k} (w :: j +-> k)
   . (Sheaf.HasFiniteCovers t j, Corepresentable w, Finitary.Finitary w, Finitary.FiniteCat j, Finitary.FiniteCat k)
  => TestTree
testCoveredByImage =
  testProperty "every object is covered by the image" $
    sequence_
      ( Finitary.foreachOb @j @(Property ()) \ @c ->
          [ expect
              ("object " ++ show (Finitary.objIndex @c) ++ " is covered")
              True
              (FinSheaf.isCovering @t (Sieve @c @'() \g _ -> or (fromImage g) \\ g))
          ]
      )
  where
    fromImage :: forall (c :: j) x. (Ob c, Ob x) => x ~> c -> [Bool]
    fromImage g =
      Finitary.foreachOb @k \ @e -> [Finitary.factorsThrough g f \\ f | x <- Finitary.elements @w @e @c, let f = coindex x]

-- | For every sieve at every pair of objects of a finite site: it is covering exactly when it is dense,
-- that is when its 'FinSheaf.closure' is the maximal sieve. Two independent computations of one
-- fact: 'FinSheaf.isCovering' reads it off the coverage, 'FinSheaf.closure' off the induced
-- topology.
testDenseIsCovering
  :: forall t j k. (Sheaf.HasFiniteCovers t k, Finitary.FiniteCat j, Finitary.FiniteCat k) => TestTree
testDenseIsCovering =
  testProperty "covering sieves are the dense ones" $
    sequence_
      ( Finitary.foreachOb @k @(Property ()) \ @a -> Finitary.foreachOb @j @(Property ()) \ @b ->
          [ expect
              ("sieve " ++ show (Finitary.toIndex s) ++ " at object " ++ show (Finitary.objIndex @a))
              (FinSheaf.isCovering @t s)
              (FinSheaf.isDense @t s)
          | s <- Finitary.elements @(Sieve :: j +-> k) @a @b
          ]
      )

-- | The uniqueness half of the sheaf condition at one cover: an element @x@ at the covered object
-- is the gluing of its own restrictions to the legs.
--
-- The other half (a glued element restricts back to the family) has no generic test: the only
-- matching family generic code can build is an element's own restrictions, and there it follows
-- from uniqueness. Other families come from the site, so that test is written per site. At a
-- finite site 'Proarrow.Category.Enriched.Finitary.Sheaf.isSheaf' decides both halves at once, by
-- checking that restriction is a bijection onto the matching families.
propGluesBack
  :: forall {j} {k} t (p :: j +-> k) (a :: k) (b :: j) c
   . (Sheaf.Sheaf t p, Ob a, Ob b, TestingEqShow (p a b))
  => Sheaf.Cover t k a c
  -> p a b
  -> Property ()
propGluesBack c x =
  testEq
    "uniqueness"
    "glue c (\\g -> lmap (legArrow g) x)"
    (Sheaf.glue @t c \g -> lmap (Sheaf.legArrow g) x)
    "x"
    x

-- | 'propGluesBack' at every cover of a random element's object. Named for the law and not for the
-- class, since it is half of what 'Sheaf.Sheaf' asks for (see 'propGluesBack' for the other half).
--
-- The object is drawn from those that actually have a cover: on a site where only some objects are
-- covered, drawing uniformly would leave most runs asserting nothing while reporting successes.
testGluesBack
  :: forall {j} {k} t (p :: j +-> k)
   . (Sheaf.HasFiniteCovers t k, Sheaf.Sheaf t p, TestableProfunctor p, TestableTypeP p, TestObIsOb k)
  => TestTree
testGluesBack = testProperty "glues back" do
  Some @a <- genObSuchThat @k \(Some @a) -> not (null (Sheaf.covers @t @k @a))
  Some @b <- genOb @j
  x <- genNamed @(p a b) "x"
  obFromTestOb @a $
    obFromTestOb @b $
      for_ (Sheaf.covers @t @k @a) \(Sheaf.SomeCover c) -> propGluesBack @t c x

-- | 'propGluesBack' at one named cover, for a site whose covers cannot be listed. The label names
-- the profunctor and cover, which nothing in the type can supply.
testGluesBackAt
  :: forall {j} {k} t (p :: j +-> k) (a :: k) c
   . (Sheaf.Sheaf t p, TestableProfunctor p, TestableTypeP p, TestOb a)
  => String
  -> Sheaf.Cover t k a c
  -> TestTree
testGluesBackAt lbl c = testProperty ("glues back at " ++ lbl) do
  Some @b <- genOb @j
  x <- genNamed @(p a b) "x"
  obFromTestOb @a $ obFromTestOb @b $ propGluesBack @t c x

-- | An equalizer of sheaves is a sheaf, for every coverage: the 'Sheaf.Sheaf' instance for
-- 'FinTopos.Reindex' presupposes that the table cuts out a /subsheaf/, and this decides it, by
-- 'FinSheaf.isSheaf', on the equalizer of every pair of parallel arrows the palette of
-- @'FinSheaf.SHEAVES' t j k@ can form.
testEqualizersAreSheaves
  :: forall t j k
   . (Testable (FinSheaf.SHEAVES t j k), Sheaf.HasFiniteCovers t k, Finitary.FiniteCat j, Finitary.FiniteCat k)
  => TestTree
testEqualizersAreSheaves = testProperty "equalizers are sheaves" do
  SomeP @a @b f <- genProfunctorElt @(Hom (FinSheaf.SHEAVES t j k)) "f"
  g <- genNamed @(a ~> b) "g"
  Equalizer.equalize f g \(Sub (Prof @e _)) -> expect "isSheaf of the equalizer" True (FinSheaf.isSheaf @t @e)

-- | Sheafification is the reflector into the sheaves: it turns a presheaf @p@ into the closest
-- sheaf, and every map from @p@ to a sheaf factors uniquely through it. For a finitary @p@ and a
-- sheaf @q@, decided at a finite site:
--
-- * @'FinSheaf.unitPlus'@ is natural;
-- * @'FinSheaf.Sheafify' t p@ is a sheaf, by 'FinSheaf.isSheaf';
-- * one plus construction fixes @q@ ('testPlusFixes');
-- * maps @'FinSheaf.Sheafify' t p ~> q@ correspond to maps @p ~> q@. The hom-sets of
--   @'FinTopos.FINITARY' j k@ are finite, so this is a count, made a bijection by
--   'FinSheaf.extendSheafify': extending every map @p ~> q@ gives every map out of the
--   sheafification once, and restricting an extension along the unit gives the map back.
--
-- This also makes 'FinSheaf.extendPlus'\'s choice of cover safe to leave unspecified: another
-- choice would show up here as an extension that is not one of the maps.
testSheafification
  :: forall t {j} {k} (p :: j +-> k) (q :: j +-> k)
   . ( Sheaf.HasFiniteCovers t k
     , Sheaf.Sheaf t q
     , Finitary.Finitary p
     , Finitary.Finitary q
     , Finitary.FiniteCat j
     , Finitary.FiniteCat k
     , Testable j
     , Testable k
     , TestableProfunctor p
     )
  => TestTree
testSheafification =
  testGroup
    "sheafification"
    [ testProperty "unit is natural" $ propNaturalTransformation @p @(FinSheaf.Plus t p) (FinSheaf.unitPlus @t)
    , testProperty "Sheafify p is a sheaf" $ expect "isSheaf" True (FinSheaf.isSheaf @t @(FinSheaf.Sheafify t p))
    , testPlusFixes @t @q
    , testProperty "left adjoint to inclusion" do
        -- both enumerations once: each is a full walk of the finitary hom-set
        let maps = FinTopos.natTransformations @p @q
            exts = FinTopos.natElements @(FinSheaf.Sheafify t p) @q
        -- a hom-set is empty whenever @q@ runs out of elements where @p@ has some, and then every
        -- assertion below holds of nothing
        expect "there are maps to extend" True (not (null maps))
        expect "as many maps out of the sheafification as out of p" (length maps) (length exts)
        expect
          "the extensions are exactly the maps out of the sheafification"
          (sort exts)
          (sort [FinTopos.natTable @(FinSheaf.Sheafify t p) @q (FinSheaf.extendSheafify @t n) | Prof n <- maps])
        for_ maps \(Prof n) ->
          expect
            "restricting an extension along the unit gives the map back"
            (FinTopos.natTable @p @q n)
            (FinTopos.natTable @p @q \x -> FinSheaf.extendSheafify @t n (FinSheaf.unitSheafify @t x))
    ]

-- | One plus leaves a sheaf as it was: 'FinSheaf.unitPlus' is a bijection at every pair of objects.
-- Stated for any finitary @q@ (the property needs no 'Sheaf.Sheaf' instance, only 'FinSheaf.isSheaf'
-- to be true of @q@), so it also serves at a coverage no profunctor has an instance for, such as the
-- trivial one, which fixes everything.
testPlusFixes
  :: forall t {j} {k} (q :: j +-> k)
   . (Sheaf.HasFiniteCovers t k, Finitary.Finitary q, Finitary.FiniteCat j, Finitary.FiniteCat k)
  => TestTree
testPlusFixes =
  testProperty "one plus fixes q" $
    sequence_
      ( Finitary.foreachOb @k @(Property ()) \ @a -> Finitary.foreachOb @j @(Property ()) \ @b ->
          -- bound once for the hom-set: at 'FinSheaf.Plus' one 'Finitary.toIndex' is an enumeration
          let ix = Finitary.toIndex @(FinSheaf.Plus t q) @a @b
          in [ expect
                 ("unit is a bijection at object " ++ show (Finitary.objIndex @a))
                 (Finitary.indices (Finitary.size @(FinSheaf.Plus t q) @a @b))
                 (sort [ix (FinSheaf.unitPlus @t x) | x <- Finitary.elements @q @a @b])
             ]
      )