{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ConstraintKinds #-}
module EndToEndSpec (spec) where

import           Prelude hiding (writeFile)
import qualified Prelude

import           Helper
import           Test.HUnit

import           System.Directory (canonicalizePath, createDirectory)
import           Data.Maybe
import           Data.List
import           Data.String.Interpolate
import           Data.String.Interpolate.Util
import           Data.Version (showVersion)

import qualified Hpack.Render as Hpack
import           Hpack.Config (packageConfig, readPackageConfig, DecodeOptions(..), DecodeResult(..), defaultDecodeOptions)
import           Hpack.Render.Hints (FormattingHints(..), sniffFormattingHints)

import qualified Paths_hpack as Hpack (version)

writeFile :: FilePath -> String -> IO ()
writeFile file c = touch file >> Prelude.writeFile file c

spec :: Spec
spec = around_ (inTempDirectoryNamed "foo") $ do
  describe "hpack" $ do
    it "ignores fields that start with an underscore" $ do
      [i|
      _foo:
        bar: 23
      library: {}
      |] `shouldRenderTo` library [i|
      other-modules:
          Paths_foo
      |]

    it "warns on duplicate fields" $ do
      [i|
      name: foo
      name: foo
      |] `shouldWarn` [
          "package.yaml: Duplicate field $.name"
        ]

    describe "spec-version" $ do
      it "accepts spec-version" $ do
        [i|
        spec-version: 0.29.5
        |] `shouldRenderTo` package [i|
        |]

      it "fails on malformed spec-version" $ do
        [i|
        spec-version: foo
        |] `shouldFailWith` "package.yaml: Error while parsing $.spec-version - invalid value \"foo\""

      it "fails on unsupported spec-version" $ do
        [i|
        spec-version: 25.0
        dependencies: foo == bar
        |] `shouldFailWith` ("The file package.yaml requires version 25.0 of the Hpack package specification, however this version of hpack only supports versions up to " ++ showVersion Hpack.version ++ ". Upgrading to the latest version of hpack may resolve this issue.")

      it "fails on unsupported spec-version from defaults" $ do
        let file = joinPath ["defaults", "sol", "hpack-template", "2017", "defaults.yaml"]
        writeFile file [i|
        spec-version: 25.0
        |]

        [i|
        defaults:
          github: sol/hpack-template
          path: defaults.yaml
          ref: "2017"
        library: {}
        |] `shouldFailWith` ("The file " ++ file ++ " requires version 25.0 of the Hpack package specification, however this version of hpack only supports versions up to " ++ showVersion Hpack.version ++ ". Upgrading to the latest version of hpack may resolve this issue.")

    describe "data-files" $ do
      it "accepts data-files" $ do
        touch "data/foo/index.html"
        touch "data/bar/index.html"
        [i|
        data-files:
          - data/**/*.html
        |] `shouldRenderTo` package [i|
        data-files:
            data/bar/index.html
            data/foo/index.html
        |]

    describe "data-dir" $ do
      it "accepts data-dir" $ do
        touch "data/foo.html"
        touch "data/bar.html"
        [i|
        data-dir: data
        data-files:
          - "*.html"
        |] `shouldRenderTo` package [i|
        data-files:
            bar.html
            foo.html
        data-dir: data
        |]

    describe "github" $ do
      it "accepts owner/repo" $ do
        [i|
        github: hspec/hspec
        |] `shouldRenderTo` package [i|
        homepage: https://github.com/hspec/hspec#readme
        bug-reports: https://github.com/hspec/hspec/issues
        source-repository head
          type: git
          location: https://github.com/hspec/hspec
        |]

      it "accepts owner/repo/path" $ do
        [i|
        github: hspec/hspec/hspec-core
        |] `shouldRenderTo` package [i|
        homepage: https://github.com/hspec/hspec#readme
        bug-reports: https://github.com/hspec/hspec/issues
        source-repository head
          type: git
          location: https://github.com/hspec/hspec
          subdir: hspec-core
        |]

    describe "homepage" $ do
      it "accepts homepage URL" $ do
        [i|
        homepage: https://example.com/
        |] `shouldRenderTo` package [i|
        homepage: https://example.com/
        |]

      context "with github" $ do
        it "gives homepage URL precedence" $ do
          [i|
          github: hspec/hspec
          homepage: https://example.com/
          |] `shouldRenderTo` package [i|
          homepage: https://example.com/
          bug-reports: https://github.com/hspec/hspec/issues
          source-repository head
            type: git
            location: https://github.com/hspec/hspec
          |]

        it "omits homepage URL if it is null" $ do
          [i|
          github: hspec/hspec
          homepage: null
          |] `shouldRenderTo` package [i|
          bug-reports: https://github.com/hspec/hspec/issues
          source-repository head
            type: git
            location: https://github.com/hspec/hspec
          |]

    describe "bug-reports" $ do
      it "accepts bug-reports URL" $ do
        [i|
        bug-reports: https://example.com/
        |] `shouldRenderTo` package [i|
        bug-reports: https://example.com/
        |]

      context "with github" $ do
        it "gives bug-reports URL precedence" $ do
          [i|
          github: hspec/hspec
          bug-reports: https://example.com/
          |] `shouldRenderTo` package [i|
          homepage: https://github.com/hspec/hspec#readme
          bug-reports: https://example.com/
          source-repository head
            type: git
            location: https://github.com/hspec/hspec
          |]

        it "omits bug-reports URL if it is null" $ do
          [i|
          github: hspec/hspec
          bug-reports: null
          |] `shouldRenderTo` package [i|
          homepage: https://github.com/hspec/hspec#readme
          source-repository head
            type: git
            location: https://github.com/hspec/hspec
          |]

    describe "defaults" $ do
      it "accepts global defaults" $ do
        writeFile "defaults/sol/hpack-template/2017/defaults.yaml" [i|
        default-extensions:
          - RecordWildCards
          - DeriveFunctor
        |]

        [i|
        defaults:
          github: sol/hpack-template
          path: defaults.yaml
          ref: "2017"
        library: {}
        |] `shouldRenderTo` library_ [i|
        default-extensions: RecordWildCards DeriveFunctor
        |]

      it "accepts library defaults" $ do
        writeFile "defaults/sol/hpack-template/2017/defaults.yaml" [i|
        exposed-modules: Foo
        |]

        [i|
        library:
          defaults:
            github: sol/hpack-template
            path: defaults.yaml
            ref: "2017"
        |] `shouldRenderTo` library [i|
        exposed-modules:
            Foo
        other-modules:
            Paths_foo
        |]

      it "accepts a list of defaults" $ do
        writeFile "defaults/foo/bar/v1/.hpack/defaults.yaml" "default-extensions: RecordWildCards"
        writeFile "defaults/foo/bar/v2/.hpack/defaults.yaml" "default-extensions: DeriveFunctor"
        [i|
        defaults:
          - foo/bar@v1
          - foo/bar@v2
        library: {}
        |] `shouldRenderTo` library_ [i|
        default-extensions: RecordWildCards DeriveFunctor
        |]

      it "accepts defaults recursively" $ do
        writeFile "defaults/foo/bar/v1/.hpack/defaults.yaml" "defaults: foo/bar@v2"
        writeFile "defaults/foo/bar/v2/.hpack/defaults.yaml" "default-extensions: DeriveFunctor"
        [i|
        defaults: foo/bar@v1
        library: {}
        |] `shouldRenderTo` library_ [i|
        default-extensions: DeriveFunctor
        |]

      it "fails on cyclic defaults" $ do
        let
          file1 = "defaults/foo/bar/v1/.hpack/defaults.yaml"
          file2 = "defaults/foo/bar/v2/.hpack/defaults.yaml"
        writeFile file1 "defaults: foo/bar@v2"
        writeFile file2 "defaults: foo/bar@v1"
        canonic1 <- canonicalizePath file1
        canonic2 <- canonicalizePath file2
        [i|
        defaults: foo/bar@v1
        library: {}
        |] `shouldFailWith` [i|cycle in defaults (#{canonic1} -> #{canonic2} -> #{canonic1})|]

      it "fails if defaults don't exist" $ do
        pending
        [i|
        defaults:
          github: sol/foo
          ref: bar
        library: {}
        |] `shouldFailWith` "Invalid value for \"defaults\"! File https://raw.githubusercontent.com/sol/foo/bar/.hpack/defaults.yaml does not exist!"

      it "fails on parse error" $ do
        let file = joinPath ["defaults", "sol", "hpack-template", "2017", "defaults.yaml"]
        writeFile file "[]"
        [i|
        defaults:
          github: sol/hpack-template
          path: defaults.yaml
          ref: "2017"
        library: {}
        |] `shouldFailWith` (file ++ ": Error while parsing $ - expected Object, encountered Array")

      it "warns on unknown fields" $ do
        let file = joinPath ["defaults", "sol", "hpack-template", "2017", "defaults.yaml"]
        writeFile file "foo: bar"
        [i|
        name: foo
        defaults:
          github: sol/hpack-template
          path: defaults.yaml
          ref: "2017"
          bar: baz
        library: {}
        |] `shouldWarn` [
            "package.yaml: Ignoring unrecognized field $.defaults.bar"
          , file ++ ": Ignoring unrecognized field $.foo"
          ]

      it "accepts defaults from local files" $ do
        writeFile "defaults/foo.yaml" [i|
        defaults:
          local: bar.yaml
        |]

        writeFile "defaults/bar.yaml" [i|
        default-extensions:
          - RecordWildCards
          - DeriveFunctor
        |]

        [i|
        defaults:
          local: defaults/foo.yaml
        library: {}
        |] `shouldRenderTo` library [i|
        other-modules:
            Paths_foo
        default-extensions: RecordWildCards DeriveFunctor
        |]

    describe "version" $ do
      it "accepts string" $ do
        [i|
        version: 0.1.0
        |] `shouldRenderTo` (package "") {packageVersion = "0.1.0"}

      it "accepts number" $ do
        [i|
        version: 0.1
        |] `shouldRenderTo` (package [i|
        |]) {packageVersion = "0.1"}

      it "rejects other values" $ do
        [i|
        version: {}
        |] `shouldFailWith` "package.yaml: Error while parsing $.version - expected Number or String, encountered Object"

    describe "license" $ do
      it "accepts cabal-style licenses" $ do
        [i|
        license: BSD3
        |] `shouldRenderTo` (package [i|
        license: BSD3
        |])

      it "accepts SPDX licenses" $ do
        [i|
        license: BSD-3-Clause
        |] `shouldRenderTo` (package [i|
        license: BSD-3-Clause
        |]) {packageCabalVersion = "2.2"}

      context "with an ambiguous license" $ do
        it "treats it as a cabal-style license" $ do
          [i|
          license: MIT
          |] `shouldRenderTo` (package [i|
          license: MIT
          |])

      context "when cabal-version >= 2.2" $ do
        it "maps license to SPDX license identifier" $ do
          [i|
          license: BSD3
          library:
            cxx-options: -Wall
          |] `shouldRenderTo` (package [i|
          license: BSD-3-Clause
          library
            other-modules:
                Paths_foo
            cxx-options: -Wall
            default-language: Haskell2010
          |]) {packageCabalVersion = "2.2"}

        it "doesn't touch unknown licenses" $ do
          [i|
          license: some-license
          library:
            cxx-options: -Wall
          |] `shouldRenderTo` (package [i|
          license: some-license
          library
            other-modules:
                Paths_foo
            cxx-options: -Wall
            default-language: Haskell2010
          |]) {packageCabalVersion = "2.2"}

      context "with a LICENSE file" $ do
        before_ (writeFile "LICENSE" license) $ do
          it "infers license" $ do
            [i|
            |] `shouldRenderTo` (package [i|
            license-file: LICENSE
            license: MIT
            |])

          context "when license can not be inferred" $ do
            it "warns" $ do
              writeFile "LICENSE" "some-licenese"
              [i|
              name: foo
              |] `shouldWarn` ["Inferring license from file LICENSE failed!"]

          context "when license is null" $ do
            it "does not infer license" $ do
              [i|
              license: null
              |] `shouldRenderTo` (package [i|
              license-file: LICENSE
              |])

    describe "build-type" $ do
      it "accept Simple" $ do
        [i|
        build-type: Simple
        |] `shouldRenderTo` (package "") {packageBuildType = "Simple"}

      it "accept Configure" $ do
        [i|
        build-type: Configure
        |] `shouldRenderTo` (package "") {packageBuildType = "Configure"}

      it "accept Make" $ do
        [i|
        build-type: Make
        |] `shouldRenderTo` (package "") {packageBuildType = "Make"}

      it "accept Custom" $ do
        [i|
        build-type: Custom
        |] `shouldRenderTo` (package "") {packageBuildType = "Custom"}

      it "rejects invalid values" $ do
        [i|
        build-type: foo
        |] `shouldFailWith` "package.yaml: Error while parsing $.build-type - expected one of Simple, Configure, Make, or Custom"


    describe "extra-doc-files" $ do
      it "accepts a list of files" $ do
        touch "CHANGES.markdown"
        touch "README.markdown"
        [i|
        extra-doc-files:
          - CHANGES.markdown
          - README.markdown
        |] `shouldRenderTo` (package [i|
        extra-doc-files:
            CHANGES.markdown
            README.markdown
        |]) {packageCabalVersion = "1.18"}

      it "accepts glob patterns" $ do
        touch "CHANGES.markdown"
        touch "README.markdown"
        [i|
        extra-doc-files:
          - "*.markdown"
        |] `shouldRenderTo` (package [i|
        extra-doc-files:
            CHANGES.markdown
            README.markdown
        |]) {packageCabalVersion = "1.18"}

      it "warns if a glob pattern does not match anything" $ do
        [i|
        name: foo
        extra-doc-files:
          - "*.markdown"
        |] `shouldWarn` ["Specified pattern \"*.markdown\" for extra-doc-files does not match any files"]

    describe "build-tools" $ do
      it "adds known build tools to build-tools" $ do
        [i|
        executable:
          build-tools:
            alex == 0.1.0
        |] `shouldRenderTo` executable_ "foo" [i|
        build-tools:
            alex ==0.1.0
        |]

      it "adds other build tools to build-tool-depends" $ do
        [i|
        executable:
          build-tools:
            hspec-discover: 0.1.0
        |] `shouldRenderTo` (executable_ "foo" [i|
        build-tool-depends:
            hspec-discover:hspec-discover ==0.1.0
        |]) {
          -- NOTE: We do not set this to 2.0 on purpose, so that the .cabal
          -- file is compatible with a wider range of Cabal versions!
          packageCabalVersion = "1.12"
        }

      context "when the name of a build tool matches an executable from the same package" $ do
        it "adds it to build-tools" $ do
          [i|
          executables:
            bar:
              build-tools:
                - bar
          |] `shouldRenderTo` executable_ "bar" [i|
          build-tools:
              bar
          |]

        it "gives per-section unqualified names precedence over global qualified names" $ do
          [i|
          build-tools:
            - foo:bar == 0.1.0
          executables:
            bar:
              build-tools:
                - bar == 0.2.0
          |] `shouldRenderTo` executable_ "bar" [i|
          build-tools:
              bar ==0.2.0
          |]

        it "gives per-section qualified names precedence over global unqualified names" $ do
          [i|
          build-tools:
            - bar == 0.1.0
          executables:
            bar:
              build-tools:
                - foo:bar == 0.2.0
          |] `shouldRenderTo` executable_ "bar" [i|
          build-tools:
              bar ==0.2.0
          |]

      context "when the name of a build tool matches a legacy system build tool" $ do
        it "adds it to build-tools" $ do
          [i|
          executable:
            build-tools:
              ghc >= 7.10
          |] `shouldRenderTo` (executable_ "foo" [i|
          build-tools:
              ghc >=7.10
          |]) { packageWarnings = ["Listing \"ghc\" under build-tools is deperecated! Please list system executables under system-build-tools instead!"] }

    describe "system-build-tools" $ do
      it "adds system build tools to build-tools" $ do
        [i|
        executable:
          system-build-tools:
            ghc >= 7.10
        |] `shouldRenderTo` executable_ "foo" [i|
        build-tools:
            ghc >=7.10
        |]

      context "with hpc" $ do
        it "infers cabal-version 1.14" $ do
          [i|
          executable:
            system-build-tools:
              hpc
          |] `shouldRenderTo` (executable_ "foo" [i|
          build-tools:
              hpc
          |]) {packageCabalVersion = "1.14"}

      context "with ghcjs" $ do
        it "infers cabal-version 1.22" $ do
          [i|
          executable:
            system-build-tools:
              ghcjs
          |] `shouldRenderTo` (executable_ "foo" [i|
          build-tools:
              ghcjs
          |]) {packageCabalVersion = "1.22"}

      context "with an unknown system build tool" $ do
        it "infers cabal-version 2.0" $ do
          [i|
          executable:
            system-build-tools:
              g++ >= 5.4.0
          |] `shouldRenderTo` (executable_ "foo" [i|
          build-tools:
              g++ >=5.4.0
          |]) {packageCabalVersion = "2.0"}

    describe "dependencies" $ do
      it "accepts single dependency" $ do
        [i|
        executable:
          dependencies: base
        |] `shouldRenderTo` executable_ "foo" [i|
        build-depends:
            base
        |]

      it "accepts list of dependencies" $ do
        [i|
        executable:
          dependencies:
            - base
            - transformers
        |] `shouldRenderTo` executable_ "foo" [i|
        build-depends:
            base
          , transformers
        |]

      context "with both global and section specific dependencies" $ do
        it "combines dependencies" $ do
          [i|
          dependencies:
            - base
          executable:
            dependencies: hspec
          |] `shouldRenderTo` executable_ "foo" [i|
          build-depends:
              base
            , hspec
          |]

        it "gives section specific dependencies precedence" $ do
          [i|
          dependencies:
            - base
          executable:
            dependencies: base >= 2
          |] `shouldRenderTo` executable_ "foo" [i|
          build-depends:
              base >=2
          |]

    describe "pkg-config-dependencies" $ do
      it "accepts pkg-config-dependencies" $ do
        [i|
        pkg-config-dependencies:
          - QtWebKit
          - weston
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        pkgconfig-depends:
            QtWebKit
          , weston
        |]

    describe "include-dirs" $ do
      it "accepts include-dirs" $ do
        [i|
        include-dirs:
          - foo
          - bar
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        include-dirs:
            foo
            bar
        |]

    describe "install-includes" $ do
      it "accepts install-includes" $ do
        [i|
        install-includes:
          - foo.h
          - bar.h
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        install-includes:
            foo.h
            bar.h
        |]

    describe "js-sources" $ before_ (touch "foo.js" >> touch "jsbits/bar.js") $ do
      it "accepts js-sources" $ do
        [i|
        executable:
          js-sources:
            - foo.js
            - jsbits/*.js
        |] `shouldRenderTo` executable_ "foo" [i|
        js-sources:
            foo.js
            jsbits/bar.js
        |]

      it "accepts global js-sources" $ do
        [i|
        js-sources:
          - foo.js
          - jsbits/*.js
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        js-sources:
            foo.js
            jsbits/bar.js
        |]

    describe "cxx-options" $ do
      it "accepts cxx-options" $ do
        [i|
        executable:
          cxx-options: -Wall
        |] `shouldRenderTo` (executable_ "foo" [i|
        cxx-options: -Wall
        |]) {packageCabalVersion = "2.2"}

      context "when used inside a nested conditional" $ do
        it "infers correct cabal-version" $ do
          [i|
          executable:
            when:
              condition: True
              when:
                condition: True
                when:
                  condition: True
                  cxx-options: -Wall
          |] `shouldRenderTo` (executable_ "foo" [i|
          if true
            if true
              if true
                cxx-options: -Wall
          |]) {packageCabalVersion = "2.2"}

    describe "cxx-sources" $ before_ (touch "foo.cc" >> touch "cxxbits/bar.cc") $ do
      it "accepts cxx-sources" $ do
        [i|
        executable:
          cxx-sources:
            - foo.cc
            - cxxbits/*.cc
        |] `shouldRenderTo` (executable_ "foo" [i|
        cxx-sources:
            foo.cc
            cxxbits/bar.cc
        |]) {packageCabalVersion = "2.2"}

    describe "extra-lib-dirs" $ do
      it "accepts extra-lib-dirs" $ do
        [i|
        extra-lib-dirs:
          - foo
          - bar
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        extra-lib-dirs:
            foo
            bar
        |]

    describe "extra-libraries" $ do
      it "accepts extra-libraries" $ do
        [i|
        extra-libraries:
          - foo
          - bar
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        extra-libraries:
            foo
            bar
        |]

    describe "extra-frameworks-dirs" $ do
      it "accepts extra-frameworks-dirs" $ do
        [i|
        extra-frameworks-dirs:
          - foo
          - bar
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        extra-frameworks-dirs:
            foo
            bar
        |]

    describe "frameworks" $ do
      it "accepts frameworks" $ do
        [i|
        frameworks:
          - foo
          - bar
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        frameworks:
            foo
            bar
        |]

    describe "c-sources" $ before_ (touch "cbits/foo.c" >> touch "cbits/bar.c" >> touch "cbits/baz.c") $ do
      it "keeps declaration order" $ do
        -- IMPORTANT: This is crucial as a workaround for https://ghc.haskell.org/trac/ghc/ticket/13786
        [i|
        library:
          c-sources:
            - cbits/foo.c
            - cbits/bar.c
            - cbits/baz.c
        |] `shouldRenderTo` library_ [i|
        c-sources:
            cbits/foo.c
            cbits/bar.c
            cbits/baz.c
        |]

      it "accepts glob patterns" $ do
        [i|
        library:
          c-sources: cbits/*.c
        |] `shouldRenderTo` library_ [i|
        c-sources:
            cbits/bar.c
            cbits/baz.c
            cbits/foo.c
        |]

      it "warns when a glob pattern does not match any files" $ do
        [i|
        name: foo
        library:
          c-sources: foo/*.c
        |] `shouldWarn` pure "Specified pattern \"foo/*.c\" for c-sources does not match any files"

      it "quotes filenames with special characters" $ do
        touch "cbits/foo bar.c"
        [i|
        library:
          c-sources:
            - cbits/foo bar.c
        |] `shouldRenderTo` library_ [i|
        c-sources:
            "cbits/foo bar.c"
        |]

    describe "custom-setup" $ do
      it "warns on unknown fields" $ do
        [i|
        name: foo
        custom-setup:
          foo: 1
          bar: 2
        |] `shouldWarn` [
            "package.yaml: Ignoring unrecognized field $.custom-setup.bar"
          , "package.yaml: Ignoring unrecognized field $.custom-setup.foo"
          ]

      it "accepts dependencies" $ do
        [i|
        custom-setup:
          dependencies:
            - base
        |] `shouldRenderTo` customSetup [i|
        setup-depends:
            base
        |]

      it "leaves build-type alone, if it exists" $ do
        [i|
        build-type: Make
        custom-setup:
          dependencies:
            - base
        |] `shouldRenderTo` (customSetup [i|
        setup-depends:
            base
        |]) {packageBuildType = "Make"}

    describe "library" $ do
      it "accepts reexported-modules" $ do
        [i|
        library:
          reexported-modules: Baz
        |] `shouldRenderTo` (library_ [i|
          reexported-modules:
              Baz
        |]) {packageCabalVersion = "1.22"}

      it "accepts signatures" $ do
        [i|
        library:
          signatures: Foo
        |] `shouldRenderTo` (library_ [i|
          signatures:
              Foo
        |]) {packageCabalVersion = "2.0"}

      context "when package.yaml contains duplicate modules" $ do
        it "generates a cabal file with duplicate modules" $ do
          -- garbage in, garbage out
          [i|
          library:
            exposed-modules: Foo
            other-modules: Foo
          |] `shouldRenderTo` library [i|
          exposed-modules:
              Foo
          other-modules:
              Foo
          |]

      context "when inferring modules" $ do
        context "with exposed-modules" $ do
          it "infers other-modules" $ do
            touch "src/Foo.hs"
            touch "src/Bar.hs"
            [i|
            library:
              source-dirs: src
              exposed-modules: Foo
            |] `shouldRenderTo` library [i|
            hs-source-dirs:
                src
            exposed-modules:
                Foo
            other-modules:
                Bar
                Paths_foo
            |]

        context "with other-modules" $ do
          it "infers exposed-modules" $ do
            touch "src/Foo.hs"
            touch "src/Bar.hs"
            [i|
            library:
              source-dirs: src
              other-modules: Bar
            |] `shouldRenderTo` library [i|
            hs-source-dirs:
                src
            exposed-modules:
                Foo
            other-modules:
                Bar
            |]

        context "with both exposed-modules and other-modules" $ do
          it "doesn't infer any modules" $ do
            touch "src/Foo.hs"
            touch "src/Bar.hs"
            [i|
            library:
              source-dirs: src
              exposed-modules: Foo
              other-modules: Bar
            |] `shouldRenderTo` library [i|
            hs-source-dirs:
                src
            exposed-modules:
                Foo
            other-modules:
                Bar
            |]

        context "with neither exposed-modules nor other-modules" $ do
          it "infers exposed-modules" $ do
            touch "src/Foo.hs"
            touch "src/Bar.hs"
            [i|
            library:
              source-dirs: src
            |] `shouldRenderTo` library [i|
            hs-source-dirs:
                src
            exposed-modules:
                Bar
                Foo
            other-modules:
                Paths_foo
            |]

        context "with a conditional" $ do
          it "doesn't infer any modules mentioned in that conditional" $ do
            touch "src/Foo.hs"
            touch "src/Bar.hs"
            [i|
            library:
              source-dirs: src
              when:
                condition: os(windows)
                exposed-modules:
                  - Foo
                  - Paths_foo
            |] `shouldRenderTo` library [i|
            hs-source-dirs:
                src
            if os(windows)
              exposed-modules:
                  Foo
                  Paths_foo
            exposed-modules:
                Bar
            |]

          context "with a source-dir inside the conditional" $ do
            it "infers other-modules" $ do
              touch "windows/Foo.hs"
              [i|
              library:
                when:
                  condition: os(windows)
                  source-dirs: windows
              |] `shouldRenderTo` library [i|
              other-modules:
                  Paths_foo
              if os(windows)
                other-modules:
                    Foo
                hs-source-dirs:
                    windows
              |]

            it "does not infer outer modules" $ do
              touch "windows/Foo.hs"
              touch "unix/Foo.hs"
              [i|
              library:
                exposed-modules: Foo
                when:
                  condition: os(windows)
                  then:
                    source-dirs: windows/
                  else:
                    source-dirs: unix/

              |] `shouldRenderTo` library [i|
              exposed-modules:
                  Foo
              other-modules:
                  Paths_foo
              if os(windows)
                hs-source-dirs:
                    windows/
              else
                hs-source-dirs:
                    unix/
              |]

        context "with generated modules" $ do
          it "includes generated modules in autogen-modules" $ do
            [i|
            library:
              generated-exposed-modules: Foo
              generated-other-modules: Bar
            |] `shouldRenderTo` (library [i|
            exposed-modules:
                Foo
            other-modules:
                Paths_foo
                Bar
            autogen-modules:
                Foo
                Bar
            |]) {packageCabalVersion = "2.0"}

          it "does not infer any mentioned generated modules" $ do
            touch "src/Exposed.hs"
            touch "src/Other.hs"
            [i|
            library:
              source-dirs: src
              generated-exposed-modules: Exposed
              generated-other-modules: Other
            |] `shouldRenderTo` (library [i|
            hs-source-dirs:
                src
            exposed-modules:
                Exposed
            other-modules:
                Paths_foo
                Other
            autogen-modules:
                Exposed
                Other
            |]) {packageCabalVersion = "2.0"}

          it "does not infer any generated modules mentioned inside conditionals" $ do
            touch "src/Exposed.hs"
            touch "src/Other.hs"
            [i|
            library:
              source-dirs: src
              when:
                condition: os(windows)
                generated-exposed-modules: Exposed
                generated-other-modules: Other
            |] `shouldRenderTo` (library [i|
            other-modules:
                Paths_foo
            hs-source-dirs:
                src
            if os(windows)
              exposed-modules:
                  Exposed
              other-modules:
                  Other
              autogen-modules:
                  Other
                  Exposed
            |]) {packageCabalVersion = "2.0"}

      context "mixins" $ do
        it "sets cabal-version to 2.0 if mixins are used" $ do
          [i|
          library:
            dependencies:
              foo:
                mixin:
                  - (Blah as Etc)
          |] `shouldRenderTo` (library [i|
          other-modules:
              Paths_foo
          build-depends:
              foo
          mixins:
              foo (Blah as Etc)
          |]) {packageCabalVersion = "2.0"}

    describe "internal-libraries" $ do
      it "accepts internal-libraries" $ do
        touch "src/Foo.hs"
        [i|
        internal-libraries:
          bar:
            source-dirs: src
          |] `shouldRenderTo` internalLibrary "bar" [i|
          exposed-modules:
              Foo
          other-modules:
              Paths_foo
          hs-source-dirs:
              src
          |]

      it "warns on unknown fields" $ do
        [i|
        name: foo
        internal-libraries:
          bar:
            baz: 42
        |] `shouldWarn` pure "package.yaml: Ignoring unrecognized field $.internal-libraries.bar.baz"

      it "warns on missing source-dirs" $ do
        [i|
        name: foo
        internal-libraries:
          bar:
            source-dirs: src
        |] `shouldWarn` pure "Specified source-dir \"src\" does not exist"

    describe "executables" $ do
      it "accepts arbitrary entry points as main" $ do
        touch "src/Foo.hs"
        touch "src/Bar.hs"
        [i|
        executables:
          foo:
            source-dirs: src
            main: Foo
        |] `shouldRenderTo` executable "foo" [i|
        main-is: Foo.hs
        ghc-options: -main-is Foo
        hs-source-dirs:
            src
        other-modules:
            Bar
            Paths_foo
        |]

      context "when inferring modules" $ do
        it "infers other-modules" $ do
          touch "src/Main.hs"
          touch "src/Foo.hs"
          [i|
          executables:
            foo:
              main: Main.hs
              source-dirs: src
          |] `shouldRenderTo` executable "foo" [i|
            main-is: Main.hs
            hs-source-dirs:
                src
            other-modules:
                Foo
                Paths_foo
          |]

        it "allows to specify other-modules" $ do
          touch "src/Foo.hs"
          touch "src/Bar.hs"
          [i|
          executables:
            foo:
              main: Main.hs
              source-dirs: src
              other-modules: Baz
          |] `shouldRenderTo` executable "foo" [i|
            main-is: Main.hs
            hs-source-dirs:
                src
            other-modules:
                Baz
          |]

        it "does not infer any mentioned generated modules" $ do
          touch "src/Foo.hs"
          [i|
          executables:
            foo:
              main: Main.hs
              source-dirs: src
              generated-other-modules: Foo
          |] `shouldRenderTo` (executable "foo" [i|
            main-is: Main.hs
            hs-source-dirs:
                src
            other-modules:
                Paths_foo
                Foo
            autogen-modules:
                Foo
          |]) {packageCabalVersion = "2.0"}

        context "with conditional" $ do
          it "doesn't infer any modules mentioned in that conditional" $ do
            touch "src/Foo.hs"
            touch "src/Bar.hs"
            [i|
            executables:
              foo:
                source-dirs: src
                when:
                  condition: os(windows)
                  other-modules: Foo
            |] `shouldRenderTo` executable "foo" [i|
            other-modules:
                Bar
                Paths_foo
            hs-source-dirs:
                src
            if os(windows)
              other-modules:
                  Foo
            |]

          it "infers other-modules" $ do
            touch "src/Foo.hs"
            touch "windows/Bar.hs"
            [i|
            executables:
              foo:
                source-dirs: src
                when:
                  condition: os(windows)
                  source-dirs: windows
            |] `shouldRenderTo` executable "foo" [i|
            other-modules:
                Foo
                Paths_foo
            hs-source-dirs:
                src
            if os(windows)
              other-modules:
                  Bar
              hs-source-dirs:
                  windows
            |]

      context "with conditional" $ do
        it "does not apply global options" $ do
          -- related bug: https://github.com/sol/hpack/issues/214
          [i|
          ghc-options: -Wall
          executables:
            foo:
              when:
                condition: os(windows)
                main: Foo.hs
          |] `shouldRenderTo` executable_ "foo" [i|
          ghc-options: -Wall
          if os(windows)
            main-is: Foo.hs
          |]

        it "accepts executable-specific fields" $ do
          [i|
          executables:
            foo:
              when:
                condition: os(windows)
                main: Foo
          |] `shouldRenderTo` executable_ "foo" [i|
          if os(windows)
            main-is: Foo.hs
            ghc-options: -main-is Foo
          |]

    describe "when" $ do
      it "accepts conditionals" $ do
        [i|
        when:
          condition: os(windows)
          dependencies: Win32
        executable: {}
        |] `shouldRenderTo` executable_ "foo" [i|
        if os(windows)
          build-depends:
              Win32
        |]
      it "warns on unknown fields" $ do
        [i|
        name: foo
        foo: 23
        when:
          - condition: os(windows)
            bar: 23
            when:
              condition: os(windows)
              bar2: 23
          - condition: os(windows)
            baz: 23
        |] `shouldWarn` [
            "package.yaml: Ignoring unrecognized field $.foo"
          , "package.yaml: Ignoring unrecognized field $.when[0].bar"
          , "package.yaml: Ignoring unrecognized field $.when[0].when.bar2"
          , "package.yaml: Ignoring unrecognized field $.when[1].baz"
          ]

      context "when parsing conditionals with else-branch" $ do
        it "accepts conditionals with else-branch" $ do
          [i|
          when:
            condition: os(windows)
            then:
              dependencies: Win32
            else:
              dependencies: unix
          executable: {}
          |] `shouldRenderTo` executable_ "foo" [i|
          if os(windows)
            build-depends:
                Win32
          else
            build-depends:
                unix
          |]

        it "rejects invalid conditionals" $ do
          [i|
          when:
            condition: os(windows)
            then:
              dependencies: Win32
            else: null
          |] `shouldFailWith` "package.yaml: Error while parsing $.when.else - expected Object, encountered Null"

        it "rejects invalid conditionals" $ do
          [i|
            dependencies:
              - foo
              - 23
          |] `shouldFailWith` "package.yaml: Error while parsing $.dependencies[1] - expected Object or String, encountered Number"

        it "warns on unknown fields" $ do
          [i|
          name: foo
          when:
            condition: os(windows)
            foo: null
            then:
              bar: null
            else:
              when:
                condition: os(windows)
                then: {}
                else:
                  baz: null
          |] `shouldWarn` [
              "package.yaml: Ignoring unrecognized field $.when.foo"
            , "package.yaml: Ignoring unrecognized field $.when.then.bar"
            , "package.yaml: Ignoring unrecognized field $.when.else.when.else.baz"
            ]

    describe "verbatim" $ do
      it "accepts strings" $ do
        [i|
        library:
          verbatim: |
            foo: 23
            bar: 42
        |] `shouldRenderTo` package [i|
        library
          other-modules:
              Paths_foo
          default-language: Haskell2010
          foo: 23
          bar: 42
        |]

      it "accepts multi-line strings as field values" $ do
        [i|
        library:
          verbatim:
            build-depneds: |
              foo
              bar
              baz
        |] `shouldRenderTo` package [i|
        library
          other-modules:
              Paths_foo
          default-language: Haskell2010
          build-depneds:
              foo
              bar
              baz
        |]

      it "allows to null out existing fields" $ do
        [i|
        library:
          verbatim:
            default-language: null
        |] `shouldRenderTo` package [i|
        library
          other-modules:
              Paths_foo
        |]

      context "when specified globally" $ do
        it "overrides header fields" $ do
          [i|
          verbatim:
            cabal-version: foo
          |] `shouldRenderTo` (package "") {packageCabalVersion = "foo"}

        it "overrides other fields" $ do
          touch "foo"
          [i|
          extra-source-files: foo
          verbatim:
            extra-source-files: bar
          |] `shouldRenderTo` package [i|
          extra-source-files: bar
          |]

        it "is not propagated into sections" $ do
          [i|
          verbatim:
            foo: 23
          library: {}
          |] `shouldRenderTo` package [i|
          foo: 23
          library
            other-modules:
                Paths_foo
            default-language: Haskell2010
          |]

      context "within a section" $ do
        it "overrides section fields" $ do
          [i|
          tests:
            spec:
              verbatim:
                type: detailed-0.9
          |] `shouldRenderTo` package [i|
          test-suite spec
            type: detailed-0.9
            other-modules:
                Paths_foo
            default-language: Haskell2010
          |]
    describe "default value of maintainer" $ do
      it "gives maintainer precedence" $ do
        [i|
          author: John Doe
          maintainer: Jane Doe
          |] `shouldRenderTo` package [i|
          author: John Doe
          maintainer: Jane Doe
          |]
      context "with author" $ do
        it "uses author if maintainer is not specified" $ do
          [i|
            author: John Doe
            |] `shouldRenderTo` package [i|
            author: John Doe
            maintainer: John Doe
            |]
        it "omits maintainer if it is null" $ do
          [i|
            author: John Doe
            maintainer: null
            |] `shouldRenderTo` package [i|
            author: John Doe
            |]

run :: HasCallStack => FilePath -> FilePath -> String -> IO ([String], String)
run userDataDir c old = run_ userDataDir c old >>= either assertFailure return

run_ :: FilePath -> FilePath -> String -> IO (Either String ([String], String))
run_ userDataDir c old = do
  mPackage <- readPackageConfig defaultDecodeOptions {decodeOptionsTarget = c, decodeOptionsUserDataDir = Just userDataDir}
  return $ case mPackage of
    Right (DecodeResult pkg cabalVersion _ warnings) ->
      let
        FormattingHints{..} = sniffFormattingHints (lines old)
        alignment = fromMaybe 0 formattingHintsAlignment
        settings = formattingHintsRenderSettings
        output = cabalVersion ++ Hpack.renderPackageWith settings alignment formattingHintsFieldOrder formattingHintsSectionsFieldOrder pkg
      in
        Right (warnings, output)
    Left err -> Left err

data RenderResult = RenderResult [String] String
  deriving Eq

instance Show RenderResult where
  show (RenderResult warnings output) = unlines (map ("WARNING: " ++) warnings) ++ output

shouldRenderTo :: HasCallStack => String -> Package -> Expectation
shouldRenderTo input p = do
  writeFile packageConfig ("name: foo\n" ++ unindent input)
  let currentDirectory = ".working-directory"
  createDirectory currentDirectory
  withCurrentDirectory currentDirectory $ do
    (warnings, output) <- run ".." (".." </> packageConfig) expected
    RenderResult warnings (dropEmptyLines output) `shouldBe` RenderResult (packageWarnings p) expected
  where
    expected = dropEmptyLines (renderPackage p)
    dropEmptyLines = unlines . filter (not . null) . lines

shouldWarn :: HasCallStack => String -> [String] -> Expectation
shouldWarn input expected = do
  writeFile packageConfig input
  (warnings, _) <- run "" packageConfig ""
  sort warnings `shouldBe` sort expected

shouldFailWith :: HasCallStack => String -> String -> Expectation
shouldFailWith input expected = do
  writeFile packageConfig input
  run_ "" packageConfig "" `shouldReturn` Left expected

customSetup :: String -> Package
customSetup a = (package content) {packageCabalVersion = "1.24", packageBuildType = "Custom"}
  where
    content = [i|
custom-setup
#{indentBy 2 $ unindent a}
|]

library_ :: String -> Package
library_ l = package content
  where
    content = [i|
library
  other-modules:
      Paths_foo
#{indentBy 2 $ unindent l}
  default-language: Haskell2010
|]

library :: String -> Package
library l = package content
  where
    content = [i|
library
#{indentBy 2 $ unindent l}
  default-language: Haskell2010
|]

internalLibrary :: String -> String -> Package
internalLibrary name e = (package content) {packageCabalVersion = "2.0"}
  where
    content = [i|
library #{name}
#{indentBy 2 $ unindent e}
  default-language: Haskell2010
|]

executable_ :: String -> String -> Package
executable_ name e = package content
  where
    content = [i|
executable #{name}
  other-modules:
      Paths_foo
#{indentBy 2 $ unindent e}
  default-language: Haskell2010
|]

executable :: String -> String -> Package
executable name e = package content
  where
    content = [i|
executable #{name}
#{indentBy 2 $ unindent e}
  default-language: Haskell2010
|]

package :: String -> Package
package c = Package "foo" "0.0.0" "Simple" "1.12" c []

data Package = Package {
  packageName :: String
, packageVersion :: String
, packageBuildType :: String
, packageCabalVersion :: String
, packageContent :: String
, packageWarnings :: [String]
}

renderPackage :: Package -> String
renderPackage Package{..} = unindent [i|
cabal-version: #{packageCabalVersion}
name: #{packageName}
version: #{packageVersion}
build-type: #{packageBuildType}

#{unindent packageContent}
|]

indentBy :: Int -> String -> String
indentBy n = unlines . map (replicate n ' ' ++) . lines

license :: String
license = [i|
Copyright (c) 2014-2018 Simon Hengel <sol@typeful.net>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
|]
