#
# Copyright:: Copyright (c) 2014-2018 Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "spec_helper"
require "shared/custom_generator_cookbook"
require "shared/setup_git_committer_config"
require "chef-dk/command/generator_commands/build_cookbook"
require "mixlib/shellout"

describe ChefDK::Command::GeneratorCommands::BuildCookbook do

  include_context("setup_git_committer_config")

  let(:argv) { %w{workflow_project} }

  let(:stdout_io) { StringIO.new }
  let(:stderr_io) { StringIO.new }

  let(:expected_cookbook_file_relpaths) do
    %w{
      kitchen.yml
      data_bags
      data_bags/keys
      data_bags/keys/delivery_builder_keys.json
      test
      test/fixtures
      test/fixtures/cookbooks
      test/fixtures/cookbooks/test
      test/fixtures/cookbooks/test/metadata.rb
      test/fixtures/cookbooks/test/recipes
      test/fixtures/cookbooks/test/recipes/default.rb
      Berksfile
      chefignore
      metadata.rb
      README.md
      LICENSE
      recipes
      recipes/default.rb
      recipes/deploy.rb
      recipes/functional.rb
      recipes/lint.rb
      recipes/provision.rb
      recipes/publish.rb
      recipes/quality.rb
      recipes/security.rb
      recipes/smoke.rb
      recipes/syntax.rb
      recipes/unit.rb
      secrets
      secrets/fakey-mcfakerton
    }
  end

  let(:expected_cookbook_files) do
    expected_cookbook_file_relpaths.map do |relpath|
      File.join(Dir["#{tempdir}"], "workflow_project", ".delivery", "build_cookbook", relpath)
    end
  end

  subject(:cookbook_generator) do
    described_class.new(argv)
  end

  def generator_context
    ChefDK::Generator.context
  end

  before do
    ChefDK::Generator.reset
  end

  it "configures the chef runner" do
    expect(cookbook_generator.chef_runner).to be_a(ChefDK::ChefRunner)
    expect(cookbook_generator.chef_runner.cookbook_path).to eq(File.expand_path("lib/chef-dk/skeletons", project_root))
  end

  context "when given invalid/incomplete arguments" do

    let(:expected_help_message) do
      "Usage: chef generate build-cookbook NAME [options]\n"
    end

    def with_argv(argv)
      generator = described_class.new(argv)
      allow(generator).to receive(:stdout).and_return(stdout_io)
      allow(generator).to receive(:stderr).and_return(stderr_io)
      generator
    end

    it "prints usage when args are empty" do
      with_argv([]).run
      expect(stderr_io.string).to include(expected_help_message)
    end

  end

  context "when given the name of the delivery project" do

    let(:argv) { %w{workflow_project} }

    let(:project_dir) { File.join(tempdir, "workflow_project") }

    before do
      reset_tempdir
      Dir.mkdir(project_dir)
    end

    it "configures the generator context" do
      cookbook_generator.read_and_validate_params
      cookbook_generator.setup_context
      expect(generator_context.workflow_project_dir).to eq(File.join(Dir.pwd, "workflow_project"))
    end

    it "creates a build cookbook" do
      Dir.chdir(tempdir) do
        allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io)
        expect(cookbook_generator.run).to eq(0)
      end
      generated_files = Dir.glob("#{tempdir}/workflow_project/**/*", File::FNM_DOTMATCH)
      expected_cookbook_files.each do |expected_file|
        expect(generated_files).to include(expected_file)
      end
    end

    shared_examples_for "a generated file" do |context_var|
      before do
        Dir.chdir(tempdir) do
          allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io)
          expect(cookbook_generator.run).to eq(0)
        end
      end

      it "should contain #{context_var} from the generator context" do
        expect(File.read(file)).to include(line)
      end
    end

    # This shared example group requires a let binding for
    # `expected_kitchen_yml_content`
    shared_examples_for "kitchen_yml_and_integration_tests" do

      describe "Generating Test Kitchen and integration testing files" do

        before do
          Dir.chdir(tempdir) do
            allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io)
            expect(cookbook_generator.run).to eq(0)
          end
        end

        let(:file) { File.join(tempdir, "workflow_project", ".delivery", "build_cookbook", "kitchen.yml") }

        it "creates a kitchen.yml with the expected content" do
          expect(IO.read(file)).to eq(expected_kitchen_yml_content)
        end

      end
    end

    context "when the delivery project is a cookbook" do

      let(:parent_metadata_rb) { File.join(tempdir, "workflow_project", "metadata.rb") }

      before do
        FileUtils.touch(parent_metadata_rb)
      end

      it "detects that the parent project is a cookbook" do
        Dir.chdir(tempdir) do
          cookbook_generator.read_and_validate_params
          cookbook_generator.setup_context
          expect(generator_context.build_cookbook_parent_is_cookbook).to eq(true)
        end
      end

      describe "metadata.rb" do
        let(:file) { File.join(tempdir, "workflow_project", ".delivery", "build_cookbook", "metadata.rb") }

        include_examples "a generated file", :cookbook_name do
          let(:line) do
            <<~METADATA
              name 'build_cookbook'
              maintainer 'The Authors'
              maintainer_email 'you@example.com'
              license 'all_rights'
              version '0.1.0'
              chef_version '>= 14.0'

              depends 'delivery-truck'
            METADATA
          end
        end
      end

      describe "delivery phase recipes" do

        before do
          Dir.chdir(tempdir) do
            allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io)
            expect(cookbook_generator.run).to eq(0)
          end
        end

        it "generates phase recipes which include the corresponding delivery truck recipe" do
          %w{
            deploy.rb
            functional.rb
            lint.rb
            provision.rb
            publish.rb
            quality.rb
            security.rb
            smoke.rb
            syntax.rb
            unit.rb
          }.each do |phase_recipe|
            recipe_file = File.join(tempdir, "workflow_project", ".delivery", "build_cookbook", "recipes", phase_recipe)
            phase = File.basename(phase_recipe, ".rb")
            expected_content = %Q{include_recipe 'delivery-truck::#{phase}'}
            expect(IO.read(recipe_file)).to include(expected_content)
          end
        end

      end

    end

    context "when the delivery project is not a cookbook" do

      it "detects that the parent project is not a cookbook" do
        cookbook_generator.read_and_validate_params
        cookbook_generator.setup_context
        expect(generator_context.build_cookbook_parent_is_cookbook).to eq(false)
      end

      describe "metadata.rb" do
        let(:file) { File.join(tempdir, "workflow_project", ".delivery", "build_cookbook", "metadata.rb") }

        include_examples "a generated file", :cookbook_name do
          let(:line) do
            <<~METADATA
              name 'build_cookbook'
              maintainer 'The Authors'
              maintainer_email 'you@example.com'
              license 'all_rights'
              version '0.1.0'
            METADATA
          end
        end
      end

      describe "delivery phase recipes" do

        before do
          Dir.chdir(tempdir) do
            allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io)
            expect(cookbook_generator.run).to eq(0)
          end
        end

        it "generates phase recipes that are empty" do
          %w{
            deploy.rb
            functional.rb
            lint.rb
            provision.rb
            publish.rb
            quality.rb
            security.rb
            smoke.rb
            syntax.rb
            unit.rb
          }.each do |phase_recipe|
            recipe_file = File.join(tempdir, "workflow_project", ".delivery", "build_cookbook", "recipes", phase_recipe)
            expect(IO.read(recipe_file)).to_not include("include_recipe")
          end
        end

      end

    end

    context "when the delivery project is a git repo" do

      let(:readme) { File.join(project_dir, "README.md") }

      def git!(cmd)
        Mixlib::ShellOut.new("git #{cmd}", cwd: project_dir).tap do |c|
          c.run_command
          c.error!
        end
      end

      before do
        FileUtils.touch(readme)

        git!("init .")
        git!("add .")
        git!("commit --no-gpg-sign -m \"initial commit\"")

        Dir.chdir(tempdir) do
          allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io)
          expect(cookbook_generator.run).to eq(0)
        end
      end

      it "creates delivery config in a feature branch and merges it" do
        expect(git!("log").stdout).to include("Merge branch 'add-delivery-configuration'")
      end

    end

    context "when the delivery project has already a config.json and project.toml" do

      let(:dot_delivery) { File.join(project_dir, ".delivery") }
      let(:config_json) { File.join(dot_delivery, "config.json") }
      let(:project_toml) { File.join(dot_delivery, "project.toml") }

      def git!(cmd)
        Mixlib::ShellOut.new("git #{cmd}", cwd: project_dir).tap do |c|
          c.run_command
          c.error!
        end
      end

      before do
        FileUtils.mkdir_p(dot_delivery)
        FileUtils.touch(config_json)
        FileUtils.touch(project_toml)

        git!("init .")
        git!("add .")
        git!("commit --no-gpg-sign -m \"initial commit\"")

        Dir.chdir(tempdir) do
          allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io)
          expect(cookbook_generator.run).to eq(0)
        end
      end

      it "does not overwrite the delivery config" do
        expect(git!("log").stdout).to_not include("Add generated delivery configuration")
      end

    end
  end

  context "when given a path including the .delivery directory" do
    let(:argv) { [ File.join(tempdir, "workflow_project", ".delivery", "build_cookbook") ] }

    before do
      reset_tempdir
    end

    it "correctly sets the delivery project dir to the parent of the .delivery dir" do
      cookbook_generator.read_and_validate_params
      cookbook_generator.setup_context
      expect(generator_context.workflow_project_dir).to eq(File.join(tempdir, "workflow_project"))
    end

  end

end
