# frozen_string_literal: true

#  Copyright (c) 2024, Schweizer Alpen-Club. This file is part of
#  hitobito and licensed under the Affero General Public License version 3
#  or later. See the COPYING file at the top-level directory or at
#  https://github.com/hitobito/hitobito

require "spec_helper"

describe Household do
  subject(:household) { Household.new(person.reload) }

  let(:person) { Fabricate(:person, first_name: "David", last_name: "Hasselhoff") }
  let(:other_person) { Fabricate(:person, first_name: "Ardona", last_name: "Mola") }
  let(:third_person) { Fabricate(:person, first_name: "Malou", last_name: "Thomas") }

  describe "#household_key" do
    it "is blank for new household" do
      expect(person.household_key).to be_blank
      expect(Household.new(person).household_key).to be_blank
    end

    it "is set for existing household" do
      create_household
      expect(person.household_key).to be_present
      expect(Household.new(person).household_key).to eq(person.household_key)
    end

    it "gets set when saving household" do
      household.add(other_person)
      expect(household.household_key).to be_blank
      household.save
      expect(household.household_key).to be_present
    end

    it "does not get set on save when household is not valid" do
      household.add(other_person)
      expect(household.household_key).to be_blank
      allow(household).to receive(:valid?).and_return(false)
      household.save
      expect(household.household_key).to be_blank
    end
  end

  describe "#people" do
    it "lists household members" do
      create_household

      expect(household.members.size).to eq(3)
      expect(household.people).to include(person)
      expect(household.people).to include(other_person)
      expect(household.people).to include(third_person)
    end
  end

  describe "#add" do
    it "returns self" do
      expect(household.add(other_person)).to eq household
    end

    it "adds person to people" do
      expect(household.people).not_to include(other_person)
      household.add(other_person)
      expect(household.people).to include(other_person)
    end

    it "does not add duplicate person" do
      expect(household.people).to include(person)
      expect(household.people).to have(1).item
      household.add(person)
      expect(household.people).to have(1).item
    end
  end

  describe "#remove" do
    it "returns self" do
      expect(household.remove(person)).to eq household
    end

    it "removes person from people" do
      expect(household.people).to include(person)
      expect(household.people).to have(1).item
      household.remove(person)
      expect(household.people).to be_empty
    end

    it "ignores unknown person" do
      expect(household.people).to include(person)
      expect(household.people).to have(1).item
      household.remove(other_person)
      expect(household.people).to have(1).item
      expect(household.people).to include(person)
    end
  end

  describe "#save" do
    it "creates new household and assigns same address to all members" do
      person.update!(street: "Greatstreet", housenumber: "345", zip_code: "3456", town: "Greattown", country: "CH")
      household.add(other_person)
      household.add(third_person)
      expect(household.new_people).to include(other_person)
      expect(household.new_people).to include(third_person)
      expect(household.save).to eq(true)
      expect(household.new_people).to be_empty

      household.reload

      expect(household.members.size).to eq(3)
      expect(household.people).to include(person)
      expect(household.people).to include(other_person)
      expect(household.people).to include(third_person)

      [person, other_person, third_person].each do |p|
        p.reload
        expect(p.address).to eq("Greatstreet 345")
        expect(p.zip_code).to eq("3456")
        expect(p.town).to eq("Greattown")
        expect(p.country).to eq("CH")
      end
    end

    it "adds person to existing household and assigns address" do
      create_household

      expect(household.members.size).to eq(3)
      fourth_person = Fabricate(:person)
      household.add(fourth_person)
      expect(household.members.size).to eq(4)
      expect(household.save).to eq(true)

      household.reload

      expect(household.members.size).to eq(4)
      expect(household.people).to include(person)
      expect(household.people).to include(other_person)
      expect(household.people).to include(third_person)
      expect(household.people).to include(fourth_person)
    end

    it "does not change anything if same person is added twice" do
      create_household

      household.add(person)
      expect(household.members.size).to eq(3)
      expect(household.save).to eq(true)

      household.reload

      expect(household.members.size).to eq(3)
    end

    it "can not be saved if not valid" do
      create_household
      fourth_person = Fabricate(:person)
      household.add(fourth_person)

      expect(household).to receive(:valid?).and_return(false)

      expect(household.save).to eq(false)
      expect(fourth_person.reload.household_key).to be_nil
    end

    it "removes person from existing household" do
      create_household
      expect(household.members.size).to eq(3)

      household.remove(third_person)
      expect(household.removed_people.count).to eq(1)
      expect(household.removed_people.first).to eq(third_person)
      expect(household.save).to eq(true)
      expect(household.removed_people).to be_empty

      expect(household.reload.members.size).to eq(2)
    end

    it "can be saved if all people removed" do
      create_household

      household.people.each do |p|
        household.remove(p)
      end
      expect(household.removed_people.count).to eq(3)
      expect(household.save).to eq(true)

      expect(Person.where(household_key: household.household_key)).not_to exist
    end

    it "destroys household if it has less than 2 members" do
      create_household

      household.remove(household.people.third)
      household.remove(household.people.second)
      expect(household.members).to have(1).item

      expect(household.save).to eq(true)

      expect(Person.where(household_key: household.household_key)).not_to exist
    end

    it "yields new_people and removed_people" do
      create_household

      household.remove(other_person)
      household.remove(third_person)
      added_person = Fabricate(:person)
      household.add(added_person)

      expect do |b|
        household.save(&b)
      end.to yield_with_args(
        contain_exactly(added_person),
        contain_exactly(other_person, third_person)
      )
    end

    it "yielded new_people and removed_people are updated" do
      expect(person.household_key).to be_blank
      create_household

      household.remove(other_person)
      added_person = Fabricate(:person)
      household.add(added_person)

      household.save! do |added_people, removed_people|
        expect(added_people.first.household_key).to eq household.household_key
        expect(added_people.first.changes).to be_empty

        expect(removed_people.first.household_key).to be_blank
        expect(removed_people.first.changes).to be_empty
      end
    end
  end

  describe "validation" do
    it "is not valid with person from other household" do
      other_household = create_other_household
      other_household_person = other_household.people.first
      other_household_person.update!(first_name: "Hans", last_name: "Halt")
      household.add(other_household_person)

      expect(household).not_to be_valid

      expect(household.errors.count).to eq(1)
      expect(household.errors.first.attribute).to eq(:"members[1].base")
      expect(household.errors.first.message).to eq("Hans Halt ist bereits Mitglied eines anderen Haushalts.")
    end

    it "is valid if only one or less people in household" do
      expect(household).to be_valid

      expect(household.errors).to be_empty
    end

    it "is not valid if it contains invalid person" do
      expect(household).to be_valid
      too_long_email = (("a" * 248) + "@example.com")
      allow(other_person).to receive(:email).and_return(too_long_email)
      expect(other_person).not_to be_valid

      household.add(other_person)

      expect(household).not_to be_valid

      expect(household.errors.count).to eq(1)
      expect(household.errors.first.attribute).to eq(:"members[1].person")
      expect(household.errors.first.message).to eq("Ardona Mola: Haupt-E-Mail ist zu lang (mehr als 255 Zeichen)")
    end
  end

  describe "address" do
    it "returns household attrs as hash" do
      person.update!(street: "Loriweg", housenumber: "42", zip_code: "6600", town: "Locarno", country: "CH")
      household.add(other_person)

      expected_attrs = {address_care_of: nil,
                        street: "Loriweg",
                        housenumber: "42",
                        postbox: nil,
                        country: "CH",
                        town: "Locarno",
                        zip_code: "6600"}

      expect(household.address_attrs).to eq(expected_attrs)
    end

    it "applies address from person with address when added to household without address" do
      create_household

      fourth_person = Fabricate(:person, street: "Loriweg", housenumber: "42")
      household.add(fourth_person)
      household.save!

      expected_attrs = {address_care_of: nil,
                        street: "Loriweg",
                        housenumber: "42",
                        postbox: nil,
                        country: nil,
                        town: nil,
                        zip_code: nil}

      expect(household.address_attrs).to eq(expected_attrs)
    end

    it "applies address for all members with invalid state when updating one person" do
      person.update!(street: "Langweilige Gasse")
      other_person.update!(street: "Lange Strasse")
      third_person.update!(street: "Breiter Weg")

      create_household # all streets are set to "Langweilige Gasse"

      other_person.update_columns(street: nil)
      expect { household.update_address! }.not_to raise_error

      expect(other_person.reload.street).to eq("Langweilige Gasse")
    end
  end

  describe "warnings" do
    it "has warning if only one or less people in household" do
      expect(household).to be_valid

      expect(household.errors).to be_empty
      expect(household.warnings.count).to eq(1)
      expect(household.warnings.first.attribute).to eq(:members)
      expect(household.warnings.first.message).to include("Der Haushalt wird aufgelöst da weniger als 2 Personen vorhanden sind.")
    end

    it "has warnings if members have different address data" do
      person.update!(street: "Loriweg", housenumber: "42", zip_code: "6600", town: "Locarno")
      household.add(other_person)

      expect(household).to be_valid

      expect(household.errors).to be_empty
      expect(household.warnings.count).to eq(1)
      expect(household.warnings.first.attribute).to eq(:members)
      expect(household.warnings.first.message).to include("Die Adresse 'Loriweg 42, 6600 Locarno' wird für alle Personen in diesem Haushalt übernommen.")
    end

    it "has warnings if reference person does not have an address but other member" do
      other_person.update!(street: "Other Loriweg", housenumber: "42", zip_code: "6600",
        town: "Locarno", country: "ch")
      household.add(other_person)

      expect(household).to be_valid

      expect(household.errors).to be_empty
      expect(household.warnings.count).to eq(1)
      expect(household.warnings.first.attribute).to eq(:members)
      expect(household.warnings.first.message).to include("Die Adresse 'Other Loriweg 42, 6600 Locarno' wird für alle Personen in diesem Haushalt übernommen.")
    end

    it "has no warning of none of the members has address" do
      person.update!(street: "", housenumber: nil, zip_code: nil, town: "   ")
      household.add(other_person)

      expect(household).to be_valid

      expect(household.errors).to be_empty
      expect(household.warnings).to be_empty
    end
  end

  describe "#destroy" do
    it "destroys whole household" do
      create_household

      household.destroy

      expect(Person.where(household_key: household.household_key)).not_to exist
    end

    it "yields removed_people" do
      create_household
      original_people = household.people.dup

      expect do |b|
        household.destroy(&b)
      end.to yield_with_args(match_array(original_people))
    end
  end

  describe "#reload" do
    it "clears instance variables" do
      household.instance_variable_set(:@dummy, "dummy")
      expect { household.reload }
        .to change { household.instance_variable_defined?(:@dummy) }
        .from(true).to(false)
    end

    it "reloads the reference person" do
      original_reference_person_name = household.reference_person.first_name
      household.reference_person.first_name = "dummy"
      expect { household.reload }
        .to change { household.reference_person.first_name }
        .from("dummy").to(original_reference_person_name)
    end

    it "reinitializes the household" do
      original_people = household.people
      household.remove(original_people.first)
      expect(household.people).not_to match_array(original_people)

      household.reload
      expect(household.people).to match_array(original_people)
    end
  end

  describe "#new_record?" do
    it "is true for new unsaved household" do
      household.add(other_person)
      expect(household.new_record?).to eq true
    end

    it "is false for saved household" do
      household.add(other_person)
      household.save!
      expect(household.new_record?).to eq false
    end

    it "is false for existing household" do
      create_household
      expect(person.household_key).to be_present
      household = Household.new(person)
      expect(household.new_record?).to eq false
    end
  end

  describe "#persisted?" do
    it "is the opposite of #new_record" do
      allow(household).to receive(:new_record?).and_return(true)
      expect(household.persisted?).to eq false

      allow(household).to receive(:new_record?).and_return(false)
      expect(household.persisted?).to eq true
    end
  end

  describe "logging", :draper_with_helpers do
    with_versioning do
      let(:top_leader) { people(:top_leader) }
      before { PaperTrail.request.whodunnit = top_leader.id }
      after { PaperTrail.request.whodunnit = nil }

      it "adds log entry for every household member when creating new household" do
        household.add(other_person)

        expect do
          household.save!
        end.to change { PaperTrail::Version.count }.by(2)

        [person, other_person].each do |person|
          log_entry = person.versions.last

          expect(log_line(log_entry)).to eq("Haushalt (Ardona Mola, David Hasselhoff) wurde neu erstellt.")

          expect(log_entry.whodunnit).to eq(top_leader.id.to_s)
          expect(log_entry.whodunnit_type).to eq(Person.sti_name)
        end
      end

      it "adds log entries for added people" do
        create_household

        fourth_person = Fabricate(:person, first_name: "Hans", last_name: "Hansen")
        household.add(fourth_person)
        fifth_person = Fabricate(:person, first_name: "Jan", last_name: "Miller")
        household.add(fifth_person)

        PaperTrail::Version.destroy_all

        expect do
          household.save!
        end.to change { PaperTrail::Version.count }.by(10)

        [person, other_person, third_person, fourth_person, fifth_person].each do |person|
          version_entries = person.versions.last(2)

          expect(log_line(version_entries.first)).to eq("Hans Hansen wurde zum Haushalt (Ardona Mola, David Hasselhoff, Malou Thomas) hinzugefügt.")
          expect(log_line(version_entries.second)).to eq("Jan Miller wurde zum Haushalt (Ardona Mola, David Hasselhoff, Malou Thomas) hinzugefügt.")

          version_entries.each do |log_entry|
            expect(log_entry.whodunnit).to eq(top_leader.id.to_s)
            expect(log_entry.whodunnit_type).to eq(Person.sti_name)
          end
        end
      end

      it "adds log entries for from household removed person" do
        create_household

        expect do
          household.remove(other_person)
          household.save!
        end.to change { PaperTrail::Version.count }.by(3)

        [person, other_person, third_person].each do |person|
          log_entry = person.versions.last

          expect(log_line(log_entry)).to eq("Ardona Mola wurde vom Haushalt (David Hasselhoff, Malou Thomas) entfernt.")

          expect(log_entry.whodunnit).to eq(top_leader.id.to_s)
          expect(log_entry.whodunnit_type).to eq(Person.sti_name)
        end
      end

      it "adds log entries for both, removed and added people" do
        create_household
        household.remove(third_person)
        household.save!
        fourth_person = Fabricate(:person, first_name: "Hans", last_name: "Hansen")
        PaperTrail::Version.destroy_all

        household.remove(other_person)
        household.add(fourth_person)

        expect do
          household.save!
        end.to change { PaperTrail::Version.count }.by(6)

        expect(collect_log_lines(person)).to eq([
          "Hans Hansen wurde zum Haushalt (David Hasselhoff) hinzugefügt.",
          "Ardona Mola wurde vom Haushalt (David Hasselhoff) entfernt."
        ])

        expect(collect_log_lines(other_person)).to eq([
          "Hans Hansen wurde zum Haushalt (David Hasselhoff) hinzugefügt.",
          "Ardona Mola wurde vom Haushalt (David Hasselhoff) entfernt."
        ])

        expect(collect_log_lines(fourth_person)).to eq([
          "Hans Hansen wurde zum Haushalt (David Hasselhoff) hinzugefügt.",
          "Ardona Mola wurde vom Haushalt (David Hasselhoff) entfernt."
        ])

        expect_whodunnit_top_leader
      end

      it "raises exception if whodunnit not set" do
        allow(PaperTrail.request).to receive(:whodunnit)

        expect do
          create_household
        end.to raise_error(RuntimeError, "PaperTrail.request.whodunnit must be set")
      end

      it "add log entry to all former members for destroyed household" do
        create_household

        expect do
          household.destroy
        end.to change { PaperTrail::Version.count }.by(3)

        [person, other_person, third_person].each do |person|
          log_entry = person.versions.last

          expect(log_line(log_entry)).to eq("Haushalt (Ardona Mola, David Hasselhoff, Malou Thomas) wurde aufgelöst.")

          expect(log_entry.whodunnit).to eq(top_leader.id.to_s)
          expect(log_entry.whodunnit_type).to eq(Person.sti_name)
        end
      end

      it "adds address changed log entry to members when creating new household" do
        person.update!(street: "Loriweg", housenumber: "42", zip_code: "6600", town: "Locarno")
        other_person # create person and remove related version entry
        PaperTrail::Version.destroy_all

        household.add(other_person)

        expect do
          household.save!
        end.to change { PaperTrail::Version.count }.by(3)

        expect(person.versions.count).to eq(1)

        # one version entry for created household, the other for changed address
        expect(other_person.versions.count).to eq(2)
        address_log_entry = other_person.versions.first

        expect(log_line(address_log_entry)).to eq("PLZ wurde auf <i>6600</i> gesetzt.</div><div>Ort wurde auf <i>Locarno</i> gesetzt.</div><div>Strasse wurde auf <i>Loriweg</i> gesetzt.</div><div>Hausnummer wurde auf <i>42</i> gesetzt.")

        expect_whodunnit_top_leader
      end
    end
  end

  private

  def create_household
    household = Household.new(person)
    household.add(other_person)
    household.add(third_person)
    household.save
    household.reload
  end

  def create_other_household
    household = Household.new(Fabricate(:person))
    household.add(Fabricate(:person))
    household.add(Fabricate(:person))
    household.save
    household.reload
  end

  def collect_log_lines(person)
    person.versions.collect do |version|
      log_line(version)
    end
  end

  def log_line(version)
    version
      .decorate
      .changes
      .gsub(/\A<div>(.*?)<\/div>\z/, '\1')
  end

  def expect_whodunnit_top_leader
    all_by_top_leader = PaperTrail::Version.all.all? do |version|
      version.whodunnit == top_leader.id.to_s &&
        version.whodunnit_type == Person.sti_name
    end
    expect(all_by_top_leader).to eq(true)
  end
end
