# frozen_string_literal: true

# rubocop:disable Metrics/BlockLength
require 'spec_helper'

module CountryDefaulter
  extend ActiveSupport::Concern

  class_methods do
    def default_country_attribute(name, country:)
      define_method("#{name}=") do |value|
        self[name] = { country: country }.merge(value)
      end
    end
  end
end

describe JsonValidator do
  describe :validate_each do
    before do
      run_migration do
        create_table(:users, force: true) do |t|
          t.text :data
          t.json :smart_data
        end
      end

      spawn_model 'User' do
        include CountryDefaulter

        schema = '
        {
          "type": "object",
          "properties": {
            "city": { "type": "string" },
            "country": { "type": "string" }
          },
          "required": ["country"]
        }
        '

        default_country_attribute :smart_data, country: 'Canada'

        serialize :data, coder: JSON
        serialize :other_data, coder: JSON
        validates :data, json: { schema: schema, message: ->(errors) { errors } }
        validates :other_data, json: { schema: schema, message: ->(errors) { errors.map { |error| error['details'].to_a.flatten.join(' ') } } }
        validates :smart_data, json: { value: ->(record, _, _) { record[:smart_data] }, schema: schema, message: ->(errors) { errors } }

        def smart_data
          OpenStruct.new(self[:smart_data])
        end
      end
    end

    context 'with valid JSON data but schema errors' do
      let(:user) do
        User.new(
          data: '{"city":"Quebec City"}',
          other_data: '{"city":"Quebec City"}',
          smart_data: { country: 'Ireland', city: 'Dublin' }
        )
      end

      specify do
        expect(user).not_to be_valid
        expect(user.errors.full_messages).to eql(['Data object at root is missing required properties: country', 'Other data missing_keys country'])
        expect(user.errors.group_by_attribute[:data].first).to have_attributes(
          options: include(errors: ['object at root is missing required properties: country'])
        )
        expect(user.errors.group_by_attribute[:other_data].first).to have_attributes(
          options: include(errors: ['object at root is missing required properties: country'])
        )
        expect(user.data).to eql({ 'city' => 'Quebec City' })
        expect(user.data_invalid_json).to be_nil
        expect(user.smart_data.city).to eql('Dublin')
        expect(user.smart_data.country).to eql('Ireland')
      end
    end

    context 'with invalid JSON data' do
      let(:data) { 'What? This is not JSON at all.' }
      let(:user) { User.new(data: data, smart_data: data) }

      specify do
        expect(user.data_invalid_json).to eql(data)
        expect(user.data).to eql({})

        # Ensure that both setters ran
        expect(user.smart_data_invalid_json).to eql(data)
        expect(user.smart_data).to eql(OpenStruct.new({ country: 'Canada' }))
      end
    end

    context 'with missing country in smart data' do
      let(:user) do
        User.new(
          data: '{"city":"Quebec City","country":"Canada"}',
          other_data: '{"city":"Quebec City","country":"Canada"}',
          smart_data: { city: 'Quebec City' }
        )
      end

      specify do
        expect(user).to be_valid
        expect(user.smart_data.city).to eql('Quebec City')
        expect(user.smart_data.country).to eql('Canada') # Due to CountryDefaulter
      end
    end
  end

  describe :schema do
    let(:validator) { JsonValidator.new(options) }
    let(:options) { { attributes: [:foo], schema: schema_option } }
    let(:schema) { validator.send(:schema, record) }

    context 'with String schema' do
      let(:schema_option) { double(:schema) }
      let(:record) { double(:record) }

      it { expect(schema).to eql(schema_option) }
    end

    context 'with Proc schema returning a Proc returning a Proc' do
      let(:schema_option) { -> { dynamic_schema } }
      let(:record) { record_class.new }
      let(:record_class) do
        Class.new do
          def dynamic_schema
            -> { another_dynamic_schema }
          end

          def another_dynamic_schema
            -> { what_another_dynamic_schema }
          end

          def what_another_dynamic_schema
            'yay'
          end
        end
      end

      it { expect(schema).to eql('yay') }
    end

    context 'with Symbol schema' do
      let(:schema_option) { :dynamic_schema }
      let(:record) { record_class.new }
      let(:record_class) do
        Class.new do
          def dynamic_schema
            'foo'
          end
        end
      end

      it { expect(schema).to eql('foo') }
    end
  end

  describe :message do
    let(:validator) { JsonValidator.new(options) }
    let(:options) { { attributes: [:foo], message: message_option } }
    let(:message) { validator.send(:message, errors) }
    let(:errors) { %i[first_error second_error] }

    context 'with Symbol message' do
      let(:message_option) { :invalid_json }
      it { expect(message).to eql([:invalid_json]) }
    end

    context 'with String value' do
      let(:message_option) { ->(errors) { errors } }
      it { expect(message).to eql(%i[first_error second_error]) }
    end
  end
end
# rubocop:enable Metrics/BlockLength
