# frozen_string_literal: true

RSpec.describe GDK::ConfigSettings do
  subject(:config) { described_class.new }

  describe '.array' do
    it 'accepts an array' do
      described_class.array(:foo) { %w[a b] }

      expect { config.foo }.not_to raise_error
    end

    it 'accepts a string' do
      described_class.array(:foo) { 'foo' }

      expect { config.foo }.not_to raise_error
    end

    it 'fails on non-array and non-string value' do
      described_class.array(:foo) { 123 }

      expect { config.foo }.to raise_error(TypeError)
    end

    context 'when there is YAML defined' do
      let(:test_klass) do
        new_test_klass do |s|
          s.array(:foo, merge: true) { %w[a] }
        end
      end

      subject(:config) { test_klass.new(yaml: { 'foo' => %w[b] }) }

      it 'is mergeable' do
        expect(config.foo).to eq(%w[b a])
      end
    end
  end

  describe '.hash_setting' do
    it 'accepts a hash' do
      described_class.hash_setting(:foo) { { 'a' => 'A' } }

      expect { config.foo }.not_to raise_error
    end

    it 'accepts a JSON parseable string' do
      described_class.hash_setting(:foo) { { 'a' => '{}' } }

      expect { config.foo }.not_to raise_error
    end

    it 'fails on non-JSON-parseable non-array value' do
      described_class.hash_setting(:foo) { %q(a b) }

      expect { config.foo }.to raise_error(GDK::StandardErrorWithMessage)
    end

    context 'when there is YAML defined' do
      let(:test_klass) do
        new_test_klass do |s|
          s.hash_setting(:foo, merge: true) { { 'a' => { 'A' => 'A' } } }
        end
      end

      context 'added values' do
        subject(:config) { test_klass.new(yaml: { 'foo' => { 'b' => { 'B' => 'B' } } }) }

        it 'values are merged' do
          expect(config.foo).to eq({ 'a' => { 'A' => 'A' }, 'b' => { 'B' => 'B' } })
        end
      end

      context 'overridden values' do
        subject(:config) { test_klass.new(yaml: { 'foo' => { 'a' => 'B' } }) }

        it 'value is overridden' do
          expect(config.foo).to eq({ 'a' => 'B' })
        end
      end

      describe '#dump!' do
        context 'when not user defined' do
          subject(:config) { test_klass.new }

          it 'dumps depending on the flag' do
            expect(config.dump!).to eq({ 'foo' => { 'a' => { 'A' => 'A' } } })
            expect(config.dump!(user_only: true)).to eq({})
          end
        end

        context 'with user defined values' do
          subject(:config) { test_klass.new(yaml: { 'foo' => { 'a' => 'B' } }) }

          it 'dumps depending on the flag' do
            expect(config.dump!).to eq({ 'foo' => { 'a' => 'B' } })
            expect(config.dump!(user_only: true)).to eq({ 'foo' => { 'a' => 'B' } })
          end
        end
      end
    end
  end

  describe '.bool' do
    it 'accepts a bool' do
      described_class.bool(:foo) { 'false' }

      expect { config.foo }.not_to raise_error
      expect(config.foo).to be(false)
    end

    it 'accepts a bool?' do
      described_class.bool(:foo) { 'false' }

      expect { config.foo? }.not_to raise_error
      expect(config.foo?).to be(false)
    end

    it 'fails on non-bool value' do
      described_class.bool(:foo) { 'hello' }

      expect { config.foo }.to raise_error(TypeError)
    end
  end

  describe '.integer' do
    it 'accepts an integer' do
      described_class.integer(:foo) { '333' }

      expect { config.foo }.not_to raise_error
      expect(config.foo).to eq(333)
    end

    it 'fails on non-integer value' do
      described_class.integer(:foo) { '33d' }

      expect { config.foo }.to raise_error(TypeError)
    end
  end

  describe '.path' do
    it 'accepts a valid path' do
      described_class.path(:foo) { '/tmp' }

      expect { config.foo }.not_to raise_error
      expect(config.foo).to be_a(Pathname)
      expect(config.foo.to_s).to eq('/tmp')
    end

    it 'fails on non-path' do
      described_class.path(:foo) { nil }

      expect { config.foo }.to raise_error(TypeError)
    end
  end

  describe '.string' do
    it 'accepts a string' do
      described_class.string(:foo) { 'howdy' }

      expect { config.foo }.not_to raise_error
      expect(config.foo).to eq('howdy')
    end

    it 'fails on non-string' do
      described_class.string(:foo) { nil }

      expect { config.foo }.to raise_error(TypeError)
    end
  end

  describe 'dynamic setting' do
    let(:test_klass) do
      new_test_klass do |s|
        s.string(:bar) { 'hello' }
      end
    end

    subject(:config) { test_klass.load_from_file }

    it 'can read a setting' do
      expect(config.bar).to eq('hello')
    end

    context 'with foo.yml' do
      before do
        File.write(test_klass::FILE, { 'bar' => 'baz' }.to_yaml)
      end

      after do
        File.unlink(test_klass::FILE)
      end

      it 'reads settings from yaml' do
        expect(config.bar).to eq('baz')
      end
    end
  end

  describe '.settings_array' do
    it 'creates an array of the desired size' do
      described_class.settings_array(:foo, size: 3) { nil }

      expect(config.foo.count).to eq(3)
    end

    it 'creates an array of the desired size using block' do
      described_class.integer(:baz) { 'four'.length }
      described_class.settings_array(:foo, size: -> { baz }) { nil }

      expect(config.foo.count).to eq(4)
    end

    it 'creates array with self as parent' do
      described_class.settings_array(:foo, size: 1) { nil }

      expect(config.foo.parent).to eq(config)
    end

    it 'creates array of settings with self as grandparent' do
      described_class.settings_array(:foo, size: 1) { nil }

      expect(config.foo.first.parent.parent).to eq(config)
    end

    it 'attributes are available through root config' do
      config = Class.new(GDK::ConfigSettings) do
        settings_array(:arrrr, size: 3) do |i|
          string(:buz) { "sub #{i}" }
        end
      end.new

      expect(config.arrrr.map(&:buz)).to eq(['sub 0', 'sub 1', 'sub 2'])
    end

    context 'indexed access' do
      let(:config) { test_class.new }

      let(:test_class) do
        new_test_klass do |s|
          s.settings :wrapped do
            settings_array(:fruits, size: 3) do |i|
              string(:name) { "apple #{i}" }
            end
          end
        end
      end

      it 'retrieves values via dig' do
        expect(config.dig('wrapped', 'fruits', 0, 'name')).to eq('apple 0')
      end

      it 'sets values via bury!' do
        config.bury!('wrapped.fruits.0.name', 'banana')

        expect(config.dig('wrapped', 'fruits', 0, 'name')).to eq('banana')
      end

      it 'checks user defined values' do
        expect(config.user_defined?('wrapped', 'fruits', 0)).to be false

        config.bury!('wrapped.fruits.0.name', 'banana')

        expect(config.user_defined?('wrapped', 'fruits', 0)).to be true
        expect(config.user_defined?('wrapped', 'fruits', 0, 'name')).to be true
      end

      it 'defines __index setting automatically' do
        expect(config.wrapped.fruits.map(&:__index)).to eq([0, 1, 2])
      end
    end

    context 'with dynamic size' do
      let(:yaml) { {} }
      let(:config) do
        Class.new(GDK::ConfigSettings) do
          settings_array(:items) do |i|
            string(:name) { "name #{i}" }
          end
        end.new(yaml: yaml)
      end

      it 'is empty' do
        expect(config.items.map(&:name)).to eq([])
      end

      context 'when configured in yaml' do
        let(:yaml) do
          { "items" => [
            { "name" => "test" },
            {}
          ] }
        end

        it 'has the configured size' do
          expect(config.items.map(&:name)).to eq(["test", "name 1"])
        end
      end
    end
  end

  describe '#validate!' do
    context 'when valid' do
      it 'returns nil' do
        described_class.integer(:foo) { '333' }
        described_class.string(:bar) { 'howdy' }

        expect(config.validate!).to be_nil
      end
    end

    context 'when invalid' do
      it 'raises exception' do
        described_class.integer(:foo) { 'a funny string' }
        described_class.string(:bar) { 'howdy' }

        expect { config.validate! }.to raise_error(TypeError)
      end
    end

    context 'with a nested port conflict' do
      let(:test_klass) do
        new_test_klass do |s|
          s.settings(:service_a) do
            port(:port, 'a') { 3333 }
          end

          s.settings(:service_b) do
            port(:port, 'b') { 3333 }
          end
        end
      end

      it 'raises an error' do
        expect { test_klass.new.validate! }.to raise_error("Value '3333' for setting 'service_b.port' is not a valid port - Port 3333 is already allocated for service 'a'.")
      end
    end
  end

  describe '#dump!' do
    it 'generates configs without ignored ones' do
      described_class.integer(:foo) { '333' }
      described_class.string(:bar) { 'howdy' }
      described_class.settings_array(:baz, size: 2) { string(:name) { 'bonza' } }

      described_class.integer(:__internal_foo) { '333' }
      described_class.integer(:questionable_foo?) { '333' }

      expect(config.dump!).to eq(
        'foo' => 333,
        'bar' => 'howdy',
        'baz' => [{ 'name' => 'bonza' }, { 'name' => 'bonza' }]
      )
    end

    context 'when includes user_only configs' do
      let(:yaml) { { 'bar' => 'whassup dude', 'baz' => [{}, { 'name' => 'fonzi' }] } }
      let(:config) { described_class.new(yaml: yaml) }

      it 'generates only user_only configs' do
        described_class.integer(:foo) { '333' }
        described_class.string(:bar) { 'howdy' }
        described_class.settings_array(:baz, size: 3) { string(:name) { 'bonza' } }

        expect(config.dump!(user_only: true)).to eq(
          'bar' => 'whassup dude',
          'baz' => [{}, { 'name' => 'fonzi' }]
        )
      end
    end
  end

  describe '#dump_as_yaml' do
    it 'generates configs' do
      described_class.integer(:foo) { '333' }
      described_class.string(:bar) { 'howdy' }
      described_class.settings_array(:baz, size: 1) { string(:name) { 'bonza' } }

      expect(config.dump_as_yaml).to eq(<<~YAML)
        ---
        bar: howdy
        baz:
        - name: bonza
        foo: 333
      YAML
    end
  end

  describe '#bury!' do
    it 'assigns value in the yaml' do
      key = 'foo'
      described_class.integer(key) { '333' }

      expect { config.bury!(key, '444') }.to change(config, key).to(444)
    end

    it 'raises an error when burying a port to a boolean' do
      key = 'foo'
      described_class.integer(key) { '333' }
      current_port = config[key]

      expect { config.bury!(key, false) }.to raise_error(TypeError, "Value 'false' for setting '#{key}' is not a valid integer.")

      expect(config[key]).to eq(current_port)
    end

    it 'buries into non-existing subsettings' do
      described_class.settings(:foo) { string(:name) { 'bonza' } }

      expect { config.bury!('foo.name', 'ripper') }
        .to change { config.yaml }.to('foo' => { 'name' => 'ripper' })
    end

    it 'buries next to existing subsettings' do
      described_class.settings(:foo) { string(:name) { 'bonza' } }
      config = described_class.new(yaml: { 'foo' => { 'location' => 'down under' } })

      expect { config.bury!('foo.name', 'ripper') }
        .to change { config.yaml }.to('foo' => { 'name' => 'ripper', 'location' => 'down under' })
    end
  end

  describe '#save_yaml!' do
    context 'with foo.yml' do
      before do
        stub_backup
      end

      after do
        File.unlink(test_klass::FILE)
      end

      let(:test_klass) do
        new_test_klass do |s|
          s.integer(:port) { 3000 }
        end
      end

      subject(:config) { test_klass.load_from_file }

      it 'saves to file' do
        expect(File).to receive(:write).and_call_original

        config.save_yaml!
      end

      it 'returns nil' do
        expect(config.save_yaml!).to be_nil
      end

      it 'saves config twice without errors' do
        config.save_yaml!

        expect { config.save_yaml! }.not_to raise_error
      end

      context 'when YAML was modified out of band' do
        let(:other_config) { test_klass.load_from_file }

        before do
          config # load the config
          other_config.save_yaml!
        end

        it 'raises an error' do
          expect { config.save_yaml! }.to raise_error(described_class::YamlModified)
        end
      end
    end
  end

  def new_test_klass
    Class.new(described_class) do
      yield(self)
      const_set(:FILE, 'tmp/foo.yml')
    end
  end
end
