# frozen_string_literal: true

RSpec.describe DiscourseAi::InferredConcepts::Applier do
  subject(:applier) { described_class.new }

  fab!(:topic) { Fabricate(:topic, title: "Ruby Programming Tutorial") }
  fab!(:post) { Fabricate(:post, raw: "This post is about advanced testing techniques") }
  fab!(:user) { Fabricate(:user, username: "dev_user") }
  fab!(:concept1) { Fabricate(:inferred_concept, name: "programming") }
  fab!(:concept2) { Fabricate(:inferred_concept, name: "testing") }
  fab!(:llm_model) { Fabricate(:fake_model) }

  before do
    SiteSetting.inferred_concepts_match_persona = -1
    SiteSetting.inferred_concepts_enabled = true

    # Set up the post's user
    post.update!(user: user)
  end

  describe "#apply_to_topic" do
    it "does nothing for blank topic or concepts" do
      expect { applier.apply_to_topic(nil, [concept1]) }.not_to raise_error
      expect { applier.apply_to_topic(topic, []) }.not_to raise_error
      expect { applier.apply_to_topic(topic, nil) }.not_to raise_error
    end

    it "associates concepts with topic" do
      applier.apply_to_topic(topic, [concept1, concept2])

      expect(topic.inferred_concepts).to include(concept1, concept2)
      expect(concept1.topics).to include(topic)
      expect(concept2.topics).to include(topic)
    end
  end

  describe "#apply_to_post" do
    it "does nothing for blank post or concepts" do
      expect { applier.apply_to_post(nil, [concept1]) }.not_to raise_error
      expect { applier.apply_to_post(post, []) }.not_to raise_error
      expect { applier.apply_to_post(post, nil) }.not_to raise_error
    end

    it "associates concepts with post" do
      applier.apply_to_post(post, [concept1, concept2])

      expect(post.inferred_concepts).to include(concept1, concept2)
      expect(concept1.posts).to include(post)
      expect(concept2.posts).to include(post)
    end
  end

  describe "#topic_content_for_analysis" do
    it "returns empty string for blank topic" do
      expect(applier.topic_content_for_analysis(nil)).to eq("")
    end

    it "extracts title and posts content" do
      # Create additional posts for the topic
      post1 = Fabricate(:post, topic: topic, post_number: 1, raw: "First post content", user: user)
      post2 = Fabricate(:post, topic: topic, post_number: 2, raw: "Second post content", user: user)

      content = applier.topic_content_for_analysis(topic)

      expect(content).to include(topic.title)
      expect(content).to include("First post content")
      expect(content).to include("Second post content")
      expect(content).to include(user.username)
      expect(content).to include("1)")
      expect(content).to include("2)")
    end

    it "limits to first 10 posts" do
      # Create 12 posts for the topic
      12.times { |i| Fabricate(:post, topic: topic, post_number: i + 1, user: user) }

      allow(Post).to receive(:where).with(topic_id: topic.id).and_call_original
      allow_any_instance_of(ActiveRecord::Relation).to receive(:limit).with(10).and_call_original

      applier.topic_content_for_analysis(topic)

      expect(Post).to have_received(:where).with(topic_id: topic.id)
    end
  end

  describe "#post_content_for_analysis" do
    it "returns empty string for blank post" do
      expect(applier.post_content_for_analysis(nil)).to eq("")
    end

    it "extracts post content with topic context" do
      content = applier.post_content_for_analysis(post)

      expect(content).to include(post.topic.title)
      expect(content).to include(post.raw)
      expect(content).to include(post.user.username)
      expect(content).to include("Topic:")
      expect(content).to include("Post by")
    end

    it "handles post without topic" do
      # Mock the post to return nil for topic
      allow(post).to receive(:topic).and_return(nil)

      content = applier.post_content_for_analysis(post)

      expect(content).to include(post.raw)
      expect(content).to include(post.user.username)
      expect(content).to include("Topic: ")
    end
  end

  describe "#match_existing_concepts" do
    let(:manager) { instance_double(DiscourseAi::InferredConcepts::Manager) }

    before do
      allow(DiscourseAi::InferredConcepts::Manager).to receive(:new).and_return(manager)
      allow(manager).to receive(:list_concepts).and_return(%w[programming testing ruby])
    end

    it "returns empty array for blank topic" do
      expect(applier.match_existing_concepts(nil)).to eq([])
    end

    it "returns empty array when no existing concepts" do
      allow(manager).to receive(:list_concepts).and_return([])

      result = applier.match_existing_concepts(topic)
      expect(result).to eq([])
    end

    it "matches concepts and applies them to topic" do
      # Test the real implementation without stubbing internal methods
      allow(InferredConcept).to receive(:where).with(name: ["programming"]).and_return([concept1])

      # Mock the LLM interaction
      persona_instance_double = instance_spy("DiscourseAi::Personas::Persona")
      bot_double = instance_spy(DiscourseAi::Personas::Bot)
      structured_output_double = instance_double("DiscourseAi::Completions::StructuredOutput")
      persona_class_double = double("PersonaClass") # rubocop:disable RSpec/VerifiedDoubles

      allow(AiPersona).to receive(:all_personas).and_return([persona_class_double])
      allow(persona_class_double).to receive(:id).and_return(SiteSetting.inferred_concepts_match_persona.to_i)
      allow(persona_class_double).to receive(:new).and_return(persona_instance_double)
      allow(persona_class_double).to receive(:default_llm_id).and_return(llm_model.id)
      allow(persona_instance_double).to receive(:class).and_return(persona_class_double)
      allow(LlmModel).to receive(:find).and_return(llm_model)
      allow(DiscourseAi::Personas::Bot).to receive(:as).and_return(bot_double)
      allow(bot_double).to receive(:reply).and_yield(
        structured_output_double,
        nil,
        :structured_output,
      )
      allow(structured_output_double).to receive(:read_buffered_property).with(
        :matching_concepts,
      ).and_return(["programming"])

      result = applier.match_existing_concepts(topic)
      expect(result).to eq([concept1])
    end
  end

  describe "#match_existing_concepts_for_post" do
    let(:manager) { instance_double(DiscourseAi::InferredConcepts::Manager) }

    before do
      allow(DiscourseAi::InferredConcepts::Manager).to receive(:new).and_return(manager)
      allow(manager).to receive(:list_concepts).and_return(%w[programming testing ruby])
    end

    it "returns empty array for blank post" do
      expect(applier.match_existing_concepts_for_post(nil)).to eq([])
    end

    it "returns empty array when no existing concepts" do
      allow(manager).to receive(:list_concepts).and_return([])

      result = applier.match_existing_concepts_for_post(post)
      expect(result).to eq([])
    end

    it "matches concepts and applies them to post" do
      # Test the real implementation without stubbing internal methods
      allow(InferredConcept).to receive(:where).with(name: ["testing"]).and_return([concept2])

      # Mock the LLM interaction
      persona_instance_double = instance_spy("DiscourseAi::Personas::Persona")
      bot_double = instance_spy(DiscourseAi::Personas::Bot)
      structured_output_double = instance_double("DiscourseAi::Completions::StructuredOutput")
      persona_class_double = double("PersonaClass") # rubocop:disable RSpec/VerifiedDoubles

      allow(AiPersona).to receive(:all_personas).and_return([persona_class_double])
      allow(persona_class_double).to receive(:id).and_return(SiteSetting.inferred_concepts_match_persona.to_i)
      allow(persona_class_double).to receive(:new).and_return(persona_instance_double)
      allow(persona_class_double).to receive(:default_llm_id).and_return(llm_model.id)
      allow(persona_instance_double).to receive(:class).and_return(persona_class_double)
      allow(LlmModel).to receive(:find).and_return(llm_model)
      allow(DiscourseAi::Personas::Bot).to receive(:as).and_return(bot_double)
      allow(bot_double).to receive(:reply).and_yield(
        structured_output_double,
        nil,
        :structured_output,
      )
      allow(structured_output_double).to receive(:read_buffered_property).with(
        :matching_concepts,
      ).and_return(["testing"])

      result = applier.match_existing_concepts_for_post(post)
      expect(result).to eq([concept2])
    end
  end

  describe "#match_concepts_to_content" do
    it "returns empty array for blank content or concept list" do
      expect(applier.match_concepts_to_content("", ["concept1"])).to eq([])
      expect(applier.match_concepts_to_content(nil, ["concept1"])).to eq([])
      expect(applier.match_concepts_to_content("content", [])).to eq([])
      expect(applier.match_concepts_to_content("content", nil)).to eq([])
    end

    it "uses ConceptMatcher persona to match concepts" do
      content = "This is about Ruby programming"
      concept_list = %w[programming testing ruby]
      structured_output_double = instance_double("DiscourseAi::Completions::StructuredOutput")

      persona_class_double = double("PersonaClass") # rubocop:disable RSpec/VerifiedDoubles
      persona_instance_double = instance_spy("DiscourseAi::Personas::Persona")
      bot_double = instance_spy(DiscourseAi::Personas::Bot)

      allow(AiPersona).to receive(:all_personas).and_return([persona_class_double])
      allow(persona_class_double).to receive(:id).and_return(SiteSetting.inferred_concepts_match_persona.to_i)
      allow(persona_class_double).to receive(:new).and_return(persona_instance_double)
      allow(persona_class_double).to receive(:default_llm_id).and_return(llm_model.id)
      allow(persona_instance_double).to receive(:class).and_return(persona_class_double)
      allow(LlmModel).to receive(:find).and_return(llm_model)
      allow(DiscourseAi::Personas::Bot).to receive(:as).and_return(bot_double)
      allow(bot_double).to receive(:reply).and_yield(
        structured_output_double,
        nil,
        :structured_output,
      )
      allow(structured_output_double).to receive(:read_buffered_property).with(
        :matching_concepts,
      ).and_return(%w[programming ruby])

      result = applier.match_concepts_to_content(content, concept_list)
      expect(result).to eq(%w[programming ruby])

      expect(bot_double).to have_received(:reply)
      expect(structured_output_double).to have_received(:read_buffered_property).with(
        :matching_concepts,
      )
    end

    it "handles no structured output gracefully" do
      content = "Test content"
      concept_list = ["concept1"]

      persona_class_double = double("PersonaClass") # rubocop:disable RSpec/VerifiedDoubles
      persona_instance_double = instance_double("DiscourseAi::Personas::Persona")
      bot_double = instance_double("DiscourseAi::Personas::Bot")

      allow(AiPersona).to receive(:all_personas).and_return([persona_class_double])
      allow(persona_class_double).to receive(:id).and_return(SiteSetting.inferred_concepts_match_persona.to_i)
      allow(persona_class_double).to receive(:new).and_return(persona_instance_double)
      allow(persona_class_double).to receive(:default_llm_id).and_return(llm_model.id)
      allow(persona_instance_double).to receive(:class).and_return(persona_class_double)
      allow(LlmModel).to receive(:find).and_return(llm_model)
      allow(DiscourseAi::Personas::Bot).to receive(:as).and_return(bot_double)
      allow(bot_double).to receive(:reply).and_yield(nil, nil, :text)

      result = applier.match_concepts_to_content(content, concept_list)
      expect(result).to eq([])
    end

    it "returns empty array when no matching concepts found" do
      content = "This is about something else"
      concept_list = %w[programming testing]
      expected_response = [['{"matching_concepts": []}']]

      persona_class_double = double("PersonaClass") # rubocop:disable RSpec/VerifiedDoubles
      persona_instance_double = instance_double("DiscourseAi::Personas::Persona")
      bot_double = instance_double("DiscourseAi::Personas::Bot")

      allow(AiPersona).to receive(:all_personas).and_return([persona_class_double])
      allow(persona_class_double).to receive(:id).and_return(SiteSetting.inferred_concepts_match_persona.to_i)
      allow(persona_class_double).to receive(:new).and_return(persona_instance_double)
      allow(persona_class_double).to receive(:default_llm_id).and_return(llm_model.id)
      allow(persona_instance_double).to receive(:class).and_return(persona_class_double)
      allow(LlmModel).to receive(:find).and_return(llm_model)
      allow(DiscourseAi::Personas::Bot).to receive(:as).and_return(bot_double)
      allow(bot_double).to receive(:reply).and_return(expected_response)

      result = applier.match_concepts_to_content(content, concept_list)
      expect(result).to eq([])
    end

    it "handles missing matching_concepts key in response" do
      content = "Test content"
      concept_list = ["concept1"]
      expected_response = [['{"other_key": ["value"]}']]

      persona_class_double = double("PersonaClass") # rubocop:disable RSpec/VerifiedDoubles
      persona_instance_double = instance_double("DiscourseAi::Personas::Persona")
      bot_double = instance_double("DiscourseAi::Personas::Bot")

      allow(AiPersona).to receive(:all_personas).and_return([persona_class_double])
      allow(persona_class_double).to receive(:id).and_return(SiteSetting.inferred_concepts_match_persona.to_i)
      allow(persona_class_double).to receive(:new).and_return(persona_instance_double)
      allow(persona_class_double).to receive(:default_llm_id).and_return(llm_model.id)
      allow(persona_instance_double).to receive(:class).and_return(persona_class_double)
      allow(LlmModel).to receive(:find).and_return(llm_model)
      allow(DiscourseAi::Personas::Bot).to receive(:as).and_return(bot_double)
      allow(bot_double).to receive(:reply).and_return(expected_response)

      result = applier.match_concepts_to_content(content, concept_list)
      expect(result).to eq([])
    end
  end
end
