module Test.Asdf.DecodeSpec where

import Control.Monad.Catch (throwM)
import Data.ByteString qualified as BS
import Data.List (find)
import Data.Massiv.Array (Array, D, Ix1)
import Data.Massiv.Array qualified as M
import Data.Text (Text, unpack)
import Effectful
import Effectful.Error.Static
import GHC.Generics (Generic)
import GHC.Int (Int64)
import Skeletest
import Skeletest.Predicate qualified as P
import Telescope.Asdf.Class
import Telescope.Asdf.Core
import Telescope.Asdf.Encoding
import Telescope.Asdf.Encoding.File
import Telescope.Asdf.Error
import Telescope.Asdf.Node
import Telescope.Asdf.Reference
import Telescope.Data.Parser
import Test.Asdf.FileSpec (ExampleFileFix (..))


spec :: Spec
spec = do
  describe "basic" basicSpec
  describe "example" exampleSpec
  describe "dkist" dkistSpec
  describe "references" referenceSpec
  describe "anchors" anchorSpec


basicSpec :: Spec
basicSpec = do
  describe "basic" $ do
    it "should parse asdf" $ do
      ExampleTreeFix tree <- getFixture
      a :: Asdf <- runAsdfM $ decodeFromTree tree
      a.library.name `shouldBe` "asdf"
      length a.history.extensions `shouldBe` 1

    it "should parse tree" $ do
      ExampleTreeFix (Tree tree) <- getFixture
      lookup "foo" tree `shouldBe` Just (Node mempty Nothing (Integer 42))
      lookup "name" tree `shouldBe` Just (Node mempty Nothing (String "Monty"))


exampleSpec :: Spec
exampleSpec = do
  it "should parse example.asdf" $ do
    inp <- BS.readFile "samples/example.asdf"
    e <- decodeM @Example inp
    e.name `shouldBe` "Monty"
    e.foo `shouldBe` 42
    e.items `shouldBe` ["one", "two", "three", "four", "five"]


data Example = Example
  { foo :: Int
  , name :: Text
  , items :: [Text]
  }
  deriving (Generic, FromAsdf, ToAsdf)


anchorSpec :: Spec
anchorSpec = do
  it "should create anchors" $ do
    let Encoded out = encodeTree "{hello: &hello world}"
    f <- runAsdfM $ splitAsdfFile out
    (_, ancs) <- runAsdfM $ streamAsdfFile f.tree f.blocks
    ancs `shouldBe` Anchors [("hello", String "world")]

  it "should throw missing anchors" $ do
    f <- runAsdfM $ do
      let tree = encodeTree "{message: *somealias}"
      pure $ AsdfFile tree mempty ""
    runAsdfM (streamAsdfFile f.tree f.blocks) `shouldSatisfy` P.throws @AsdfError P.anything

  it "should throw if alias before anchor" $ do
    let Encoded out = encodeTree "{message: *hello, hello: &hello world}"
    decodeM @Tree out `shouldSatisfy` P.throws @AsdfError P.anything

  it "should succeed if anchor before alias" $ do
    let Encoded out = encodeTree "{hello: &hello world, message: *hello}"
    Tree tree <- decodeM @Tree out
    lookup "message" tree `shouldBe` Just "world"

  it "should decode anchors roundtrip" $ do
    let root = [("hello", Node mempty (Just "hello") (String "world")), ("message", toNode $ Alias "hello")] :: Object
    out <- runAsdfM $ encode (Object root)
    Tree tree <- decodeM @Tree out
    lookup "message" tree `shouldBe` Just "world"


referenceSpec :: Spec
referenceSpec = do
  it "should parse pointers" $ do
    jsonPointer "/users/1/name" `shouldBe` JSONPointer (Path [Child "users", Index 1, Child "name"])
    jsonPointer "" `shouldBe` JSONPointer (Path [])
    jsonPointer "#" `shouldBe` JSONPointer (Path [])
    jsonPointer "/" `shouldBe` JSONPointer (Path [])
    jsonPointer "users" `shouldBe` JSONPointer (Path [Child "users"])
    jsonPointer "/users" `shouldBe` JSONPointer (Path [Child "users"])
    jsonPointer "#users" `shouldBe` JSONPointer (Path [Child "users"])

  it "should parse an internal pointer as a reference" $ do
    jsonReference "#/users/1/name" `shouldBe` JSONReference mempty (jsonPointer "#/users/1/name")

  it "should parse an external reference" $ do
    let url = "https://woot.com/asdf"
    let point = "#/users/1/name"
    jsonReference (url <> point) `shouldBe` JSONReference url (jsonPointer point)

  it "should show pointers" $ do
    let point = JSONPointer (Path [Child "users", Index 0, Child "name"])
    show point `shouldBe` "#/users/0/name"

  it "should show references" $ do
    let point = JSONPointer (Path [Child "users", Index 0, Child "name"])
    let uri = "https://example.com/document.asdf/"
    show (JSONReference uri point) `shouldBe` "https://example.com/document.asdf/#/users/0/name"

  it "should locate pointer" $ do
    RefTreeFix tree <- getFixture
    n0 <- parseIO $ findPointer (jsonPointer "#/users/0/name") tree
    n0 `shouldBe` "Monty"

    n1 <- parseIO $ findPointer (jsonPointer "/users/1/name") tree
    n1 `shouldBe` "Harold"


-- I don't think we should automatically resolve any internal references. Assume all references are external
-- it "parses Internal Ref to CurrentUsername with tree" $ do
--   RefTreeFix (Tree tree) <- getFixture
--   cu <- runParse $ runAsdfParser (Tree tree) $ parseValue @CurrentUsername (InternalRef $ pointer "#/users/2/name")
--   cu `shouldBe` CurrentUsername "Sandra"
--
-- it "parses Internal Ref to RefResolved with tree" $ do
--   RefTreeFix (Tree tree) <- getFixture
--   r <- runParse $ runAsdfParser (Tree tree) $ parseValue @RefResolved (Object tree)
--   length r.users `shouldBe` 3
--   r.currentUsername `shouldBe` CurrentUsername "Harold"
--
-- it "should parse internal references from sample file" $ do
--   inp <- BS.readFile "./samples/reference.asdf"
--   r <- decodeM @RefResolved inp
--   length r.users `shouldBe` 3
--   fmap (.name) r.users `shouldBe` ["Monty", "Harold", "Sandra"]
--   r.currentUsername `shouldBe` CurrentUsername "Harold"

-- data RefResolved = RefResolved
--   { currentUsername :: CurrentUsername
--   , users :: [RefUser]
--   }
--   deriving (Generic, FromAsdf, Show)
--
--
-- data RefUser = RefUser
--   { name :: Text
--   }
--   deriving (Generic, FromAsdf, Show)

-- newtype CurrentUsername = CurrentUsername Text
--   deriving (Show, Eq)
-- instance FromAsdf CurrentUsername where
--   parseValue = \case
--     String s -> pure $ CurrentUsername s
--     -- TODO: this should be automagic
--     InternalRef p -> parsePointer p
--     val -> expected "UsernameRef" val
-- instance ToAsdf CurrentUsername where
--   toValue (CurrentUsername _) = InternalRef $ pointer "/users/2/name"

dkistSpec :: Spec
dkistSpec = do
  it "should parse dkist asdf" $ do
    inp <- BS.readFile "samples/dkist.asdf"
    d <- decodeM @DKISTAsdf inp
    d.dataset.unit `shouldBe` Count
    d.dataset.meta.inventory.datasetId `shouldBe` "AVORO"

    let us = d.dataset.meta.headers.bunit
    take 3 us `shouldBe` ["ct", "ct", "ct"]

    take 3 (M.toLists d.dataset.meta.headers.naxis2) `shouldBe` [998, 998, 998]


data DKISTAsdf = DKISTAsdf
  { dataset :: Dataset
  }
  deriving (Generic, FromAsdf)


data Dataset = Dataset
  { unit :: Unit
  , meta :: Meta
  }
  deriving (Generic, FromAsdf)


data Meta = Meta
  { headers :: MetaHeaders
  , inventory :: MetaInventory
  }
  deriving (Generic, FromAsdf)


data MetaInventory = MetaInventory
  { bucket :: Text
  , datasetId :: Text
  }
  deriving (Generic, FromAsdf)


-- can we make this work with a generic?
data MetaHeaders = MetaHeaders
  { naxis :: Array D Ix1 Int64
  , naxis2 :: Array D Ix1 Int64
  , bitpix :: [Int64]
  , bunit :: [Text]
  }


instance FromAsdf MetaHeaders where
  parseValue = \case
    Object o -> do
      ns <- o .: "columns"
      naxis <- parseColumn "NAXIS" ns
      naxis2 <- parseColumn "NAXIS2" ns
      bitpix <- parseColumn "BITPIX" ns
      bunit <- parseColumn "BUNIT" ns
      pure MetaHeaders{naxis, naxis2, bitpix, bunit}
    val -> expected "Columns" val
   where
    parseColumn :: forall a es. (FromAsdf a, Parser :> es) => Text -> [Node] -> Eff es a
    parseColumn name ns = do
      case find (isColumnName name) ns of
        Just (Node _ _ (Object o)) ->
          o .: "data"
        _ -> parseFail $ "Column " ++ unpack name ++ " not found"

    isColumnName n = \case
      Node _ _ (Object o) -> do
        lookup "name" o == Just (Node mempty Nothing (String n))
      _ -> False


newtype ExampleTreeFix = ExampleTreeFix Tree
instance Fixture ExampleTreeFix where
  fixtureAction = do
    ExampleFileFix _ f <- getFixture
    tree <- runAsdfM $ parseAsdfTree f.tree f.blocks
    pure $ noCleanup $ ExampleTreeFix tree


newtype RefTreeFix = RefTreeFix Tree
instance Fixture RefTreeFix where
  fixtureAction = do
    let user n = toNode $ Object [("name", toNode (String n))]
    let users = toNode $ Array [user "Monty", user "Harold", user "Sandra"]
    let curr = toNode $ Reference $ JSONReference mempty (jsonPointer "#/users/1/name")
    let tree = Tree [("users", users), ("currentUsername", toNode curr)]
    pure $ noCleanup $ RefTreeFix tree


parseIO :: Eff '[Parser, Error ParseError, IOE] a -> IO a
parseIO p = runEff $ runErrorNoCallStackWith @ParseError throwM $ runParser p