# typed: false
require "rails_helper"

RSpec.describe Domain::PostFile::BitFingerprint, type: :model do
  describe ".similar_to_fingerprint" do
    let(:image_paths) do
      {
        # Map original images to their low-quality versions
        "images/thumb-036aaab6-content-container.jpeg" =>
          "images/thumb-036aaab6-low-quality.jpeg",
        "images/thumb-ac63d9d7-content-container.jpeg" =>
          "images/thumb-ac63d9d7-low-quality.jpeg",
        "images/thumb-c8feb8a92-content-container.jpeg" =>
          "images/thumb-c8feb8a92-low-quality.jpeg",
      }
    end

    before(:each) do
      # Create posts and post_files with the original images
      @fingerprints = []

      image_paths.keys.each_with_index do |image_path, index|
        # Create a blob file with the image content
        log_entry =
          create(
            :http_log_entry,
            content_type: "image/jpeg",
            response:
              create(
                :blob_file,
                content_type: "image/jpeg",
                contents:
                  File.read(
                    Rails.root.join("test/fixtures/files", image_path),
                    mode: "rb",
                  ),
              ),
          )

        # Create a post file with the blob
        post_file = create(:domain_post_file, state: "ok", log_entry:)
        thumbs = Domain::PostFile::Thumbnail.create_for_post_file!(post_file)
        expect(thumbs.size).to eq(2)
        fingerprints = described_class.create_for_post_file!(post_file)
        expect(fingerprints.size).to eq(1)
        @fingerprints << fingerprints.first
      end
    end

    it "returns similar images in order of similarity" do
      # For each low-quality image, generate a fingerprint and test that similar_to_fingerprint
      # returns the correct original image fingerprint first

      image_paths.each_with_index do |(original_path, low_quality_path), index|
        # Read the low-quality image
        low_quality_image_path =
          Rails.root.join("test/fixtures/files", low_quality_path)

        # Generate a fingerprint for the low-quality image
        fingerprint = DHashVips::IDHash.fingerprint(low_quality_image_path.to_s)
        fingerprint_value =
          fingerprint.to_s(2).rjust(
            Domain::PostFile::BitFingerprint::HASH_SIZE_BYTES * 8,
            "0",
          )

        # Find similar fingerprints
        similar_fingerprints =
          described_class.order_by_fingerprint_distance(fingerprint_value).to_a

        # The original image's fingerprint should be in the top results
        # The PostgreSQL operator <~> (hamming distance) may produce ties
        # when two fingerprints have the exact same distance, resulting in
        # arbitrary ordering among equal-distance results
        expected_fingerprint = @fingerprints[index]
        found_index = similar_fingerprints.find_index(expected_fingerprint)

        # Get the distance for the first result to compare with our expected result
        first_result_distance =
          described_class.hamming_distance(
            fingerprint_value,
            similar_fingerprints.first.fingerprint_value,
          )

        # Get the distance for our expected fingerprint
        expected_distance =
          described_class.hamming_distance(
            fingerprint_value,
            expected_fingerprint.fingerprint_value,
          )

        # Verify our expected fingerprint is in the results and has the same or
        # very close distance to the first result
        expect(similar_fingerprints).to include(expected_fingerprint)
        expect(found_index).to be <= 1,
        "Expected fingerprint #{expected_fingerprint.id} should be among the first two results"
        expect(expected_distance).to be_within(1).of(first_result_distance),
        "Expected distance (#{expected_distance}) should match the first result's distance (#{first_result_distance})"

        # Calculate similarity for verification
        similarity =
          similar_fingerprints.first.similarity_percentage_to(
            @fingerprints[index],
          )
        expect(similarity).to be > 70 # Expect at least 70% similarity

        # Check that this low-quality image has low similarity with OTHER original images
        other_fingerprint_indices = (0...@fingerprints.size).to_a - [index]
        other_fingerprint_indices.each do |other_index|
          # Generate a fingerprint from the low-quality image
          low_quality_fingerprint = described_class.new
          low_quality_fingerprint.fingerprint_value = fingerprint_value

          # Compare with an original image that it should NOT match
          other_similarity =
            low_quality_fingerprint.similarity_percentage_to(
              @fingerprints[other_index],
            )

          # Should have lower similarity to non-matching original images
          expect(other_similarity).to be < 70,
          "Low-quality version of image #{index} should not be similar to original image #{other_index}"
        end
      end
    end

    it "calculates correct hamming distance between fingerprints" do
      # Test the hamming_distance_to method
      reference_fingerprint = @fingerprints.first

      # A fingerprint should have zero distance to itself
      expect(
        reference_fingerprint.hamming_distance_to(reference_fingerprint),
      ).to eq(0)

      # Different fingerprints should have non-zero distance
      expect(
        reference_fingerprint.hamming_distance_to(@fingerprints[1]),
      ).to be > 0

      # Test the hamming_distance class method
      expect(
        described_class.hamming_distance(
          reference_fingerprint.fingerprint_value,
          @fingerprints[1].fingerprint_value,
        ),
      ).to be > 0
    end

    it "calculates correct similarity percentage" do
      # Test the similarity_percentage_to method
      reference_fingerprint = @fingerprints.first

      # A fingerprint should be 100% similar to itself
      expect(
        reference_fingerprint.similarity_percentage_to(reference_fingerprint),
      ).to eq(100)

      # Different fingerprints should have less than 100% similarity
      expect(
        reference_fingerprint.similarity_percentage_to(@fingerprints[1]),
      ).to be < 100
    end
  end

  describe ".from_file_path" do
    it "creates a fingerprint from a file path" do
      # Use one of the test images
      file_path =
        Rails
          .root
          .join(
            "test/fixtures/files/images/thumb-036aaab6-content-container.jpeg",
          )
          .to_s

      # Create a fingerprint from the file path
      fingerprint = described_class.from_file_path(file_path)

      # Check that the fingerprint was created properly
      expect(fingerprint).to be_a(String)
      expect(fingerprint.length).to eq(
        Domain::PostFile::BitFingerprint::HASH_SIZE_BYTES * 8,
      )
    end

    it "raises an error for non-existent files" do
      expect {
        described_class.from_file_path("/non/existent/file.jpg")
      }.to raise_error(ArgumentError, /File does not exist/)
    end
  end
end
