# frozen_string_literal: true

RSpec.describe DiscourseAi::Admin::AiPersonasController do
  fab!(:admin)
  fab!(:ai_persona)
  fab!(:embedding_definition)
  fab!(:llm_model)

  before do
    sign_in(admin)
    SiteSetting.ai_embeddings_selected_model = embedding_definition.id
    SiteSetting.ai_embeddings_enabled = true
  end

  describe "GET #index" do
    it "returns a success response" do
      get "/admin/plugins/discourse-ai/ai-personas.json"
      expect(response).to be_successful

      expect(response.parsed_body["ai_personas"].length).to eq(AiPersona.count)
      expect(response.parsed_body["meta"]["tools"].length).to eq(
        DiscourseAi::Personas::Persona.all_available_tools.length,
      )
    end

    it "sideloads llms" do
      get "/admin/plugins/discourse-ai/ai-personas.json"
      expect(response).to be_successful

      expect(response.parsed_body["meta"]["llms"]).to eq(
        [
          {
            id: llm_model.id,
            name: llm_model.display_name,
            vision_enabled: llm_model.vision_enabled,
          }.stringify_keys,
        ],
      )
    end

    it "returns tool options with each tool" do
      persona1 = Fabricate(:ai_persona, name: "search1", tools: ["SearchCommand"])
      persona2 =
        Fabricate(
          :ai_persona,
          name: "search2",
          tools: [["SearchCommand", { base_query: "test" }, true]],
          allow_topic_mentions: true,
          allow_personal_messages: true,
          allow_chat_channel_mentions: true,
          allow_chat_direct_messages: true,
          default_llm_id: llm_model.id,
          question_consolidator_llm_id: llm_model.id,
          forced_tool_count: 2,
        )
      persona2.create_user!

      get "/admin/plugins/discourse-ai/ai-personas.json"
      expect(response).to be_successful

      serializer_persona1 = response.parsed_body["ai_personas"].find { |p| p["id"] == persona1.id }
      serializer_persona2 = response.parsed_body["ai_personas"].find { |p| p["id"] == persona2.id }

      expect(serializer_persona2["allow_topic_mentions"]).to eq(true)
      expect(serializer_persona2["allow_personal_messages"]).to eq(true)
      expect(serializer_persona2["allow_chat_channel_mentions"]).to eq(true)
      expect(serializer_persona2["allow_chat_direct_messages"]).to eq(true)

      expect(serializer_persona2["default_llm_id"]).to eq(llm_model.id)
      expect(serializer_persona2["question_consolidator_llm_id"]).to eq(llm_model.id)
      expect(serializer_persona2["user_id"]).to eq(persona2.user_id)
      expect(serializer_persona2["user"]["id"]).to eq(persona2.user_id)
      expect(serializer_persona2["forced_tool_count"]).to eq(2)

      tools = response.parsed_body["meta"]["tools"]
      search_tool = tools.find { |c| c["id"] == "Search" }

      expect(search_tool["help"]).to eq(I18n.t("discourse_ai.ai_bot.tool_help.search"))

      expect(search_tool["options"]).to eq(
        {
          "base_query" => {
            "type" => "string",
            "name" => I18n.t("discourse_ai.ai_bot.tool_options.search.base_query.name"),
            "description" =>
              I18n.t("discourse_ai.ai_bot.tool_options.search.base_query.description"),
          },
          "max_results" => {
            "type" => "integer",
            "name" => I18n.t("discourse_ai.ai_bot.tool_options.search.max_results.name"),
            "description" =>
              I18n.t("discourse_ai.ai_bot.tool_options.search.max_results.description"),
          },
          "search_private" => {
            "type" => "boolean",
            "name" => I18n.t("discourse_ai.ai_bot.tool_options.search.search_private.name"),
            "description" =>
              I18n.t("discourse_ai.ai_bot.tool_options.search.search_private.description"),
          },
        },
      )

      expect(serializer_persona1["tools"]).to eq(["SearchCommand"])
      expect(serializer_persona2["tools"]).to eq(
        [["SearchCommand", { "base_query" => "test" }, true]],
      )
    end

    context "with translations" do
      before do
        SiteSetting.default_locale = "fr"

        TranslationOverride.upsert!(
          SiteSetting.default_locale,
          "discourse_ai.ai_bot.personas.general.name",
          "Général",
        )
        TranslationOverride.upsert!(
          SiteSetting.default_locale,
          "discourse_ai.ai_bot.personas.general.description",
          "Général Description",
        )
      end

      after do
        TranslationOverride.revert!(
          SiteSetting.default_locale,
          "discourse_ai.ai_bot.personas.general.name",
        )
        TranslationOverride.revert!(
          SiteSetting.default_locale,
          "discourse_ai.ai_bot.personas.general.description",
        )
      end

      it "returns localized persona names and descriptions" do
        get "/admin/plugins/discourse-ai/ai-personas.json"

        id = DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General]
        persona = response.parsed_body["ai_personas"].find { |p| p["id"] == id }

        expect(persona["name"]).to eq("Général")
        expect(persona["description"]).to eq("Général Description")
      end
    end
  end

  describe "GET #edit" do
    it "returns a success response" do
      get "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}/edit.json"
      expect(response).to be_successful
      expect(response.parsed_body["ai_persona"]["name"]).to eq(ai_persona.name)
    end

    it "includes rag uploads for each persona" do
      upload = Fabricate(:upload)
      RagDocumentFragment.link_target_and_uploads(ai_persona, [upload.id])

      get "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}/edit.json"
      expect(response).to be_successful

      serialized_persona = response.parsed_body["ai_persona"]

      expect(serialized_persona.dig("rag_uploads", 0, "id")).to eq(upload.id)
      expect(serialized_persona.dig("rag_uploads", 0, "original_filename")).to eq(
        upload.original_filename,
      )
    end
  end

  describe "POST #create" do
    context "with valid params" do
      let(:valid_attributes) do
        {
          name: "superbot",
          description: "Assists with tasks",
          system_prompt: "you are a helpful bot",
          tools: [["search", { "base_query" => "test" }, true]],
          top_p: 0.1,
          temperature: 0.5,
          allow_topic_mentions: true,
          allow_personal_messages: true,
          allow_chat_channel_mentions: true,
          allow_chat_direct_messages: true,
          default_llm_id: llm_model.id,
          question_consolidator_llm_id: llm_model.id,
          forced_tool_count: 2,
          response_format: [{ key: "summary", type: "string" }],
          examples: [%w[user_msg1 assistant_msg1], %w[user_msg2 assistant_msg2]],
        }
      end

      it "creates a new AiPersona" do
        expect {
          post "/admin/plugins/discourse-ai/ai-personas.json",
               params: { ai_persona: valid_attributes }.to_json,
               headers: {
                 "CONTENT_TYPE" => "application/json",
               }

          expect(response).to be_successful
          persona_json = response.parsed_body["ai_persona"]

          expect(persona_json["name"]).to eq("superbot")
          expect(persona_json["top_p"]).to eq(0.1)
          expect(persona_json["temperature"]).to eq(0.5)
          expect(persona_json["default_llm_id"]).to eq(llm_model.id)
          expect(persona_json["forced_tool_count"]).to eq(2)
          expect(persona_json["allow_topic_mentions"]).to eq(true)
          expect(persona_json["allow_personal_messages"]).to eq(true)
          expect(persona_json["allow_chat_channel_mentions"]).to eq(true)
          expect(persona_json["allow_chat_direct_messages"]).to eq(true)
          expect(persona_json["question_consolidator_llm_id"]).to eq(llm_model.id)
          expect(persona_json["response_format"].map { |rf| rf["key"] }).to contain_exactly(
            "summary",
          )
          expect(persona_json["examples"]).to eq(valid_attributes[:examples])

          persona = AiPersona.find(persona_json["id"])

          expect(persona.tools).to eq([["search", { "base_query" => "test" }, true]])
          expect(persona.top_p).to eq(0.1)
          expect(persona.temperature).to eq(0.5)
        }.to change(AiPersona, :count).by(1)
      end

      it "logs staff action when creating a persona" do
        # Create the persona
        post "/admin/plugins/discourse-ai/ai-personas.json",
             params: { ai_persona: valid_attributes }.to_json,
             headers: {
               "CONTENT_TYPE" => "application/json",
             }

        expect(response).to be_successful

        # Now verify the log was created with the right subject
        history =
          UserHistory.where(
            action: UserHistory.actions[:custom_staff],
            custom_type: "create_ai_persona",
          ).last
        expect(history).to be_present
        expect(history.subject).to eq("superbot") # Verify subject is set to name
      end
    end

    context "with invalid params" do
      it "renders a JSON response with errors for the new ai_persona" do
        post "/admin/plugins/discourse-ai/ai-personas.json", params: { ai_persona: { foo: "" } } # invalid attribute
        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.content_type).to include("application/json")
      end
    end
  end

  describe "POST #create_user" do
    it "creates a user for the persona" do
      post "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}/create-user.json"
      ai_persona.reload

      expect(response).to be_successful
      expect(response.parsed_body["user"]["id"]).to eq(ai_persona.user_id)
    end
  end

  describe "PUT #update" do
    context "with scoped api key" do
      it "allows updates with a properly scoped API key" do
        api_key = Fabricate(:api_key, user: admin, created_by: admin)

        scope =
          ApiKeyScope.create!(
            resource: "discourse_ai",
            action: "update_personas",
            api_key_id: api_key.id,
            allowed_parameters: {
            },
          )

        put "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}.json",
            params: {
              ai_persona: {
                name: "UpdatedByAPI",
                description: "Updated via API key",
              },
            },
            headers: {
              "Api-Key" => api_key.key,
              "Api-Username" => admin.username,
            }

        expect(response).to have_http_status(:ok)
        ai_persona.reload
        expect(ai_persona.name).to eq("UpdatedByAPI")
        expect(ai_persona.description).to eq("Updated via API key")

        scope.update!(action: "fake")

        put "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}.json",
            params: {
              ai_persona: {
                name: "UpdatedByAPI 2",
                description: "Updated via API key",
              },
            },
            headers: {
              "Api-Key" => api_key.key,
              "Api-Username" => admin.username,
            }

        expect(response).not_to have_http_status(:ok)
      end
    end

    it "allows us to trivially clear top_p and temperature" do
      persona = Fabricate(:ai_persona, name: "test_bot2", top_p: 0.5, temperature: 0.1)
      put "/admin/plugins/discourse-ai/ai-personas/#{persona.id}.json",
          params: {
            ai_persona: {
              top_p: "",
              temperature: "",
            },
          }

      expect(response).to have_http_status(:ok)
      persona.reload

      expect(persona.top_p).to eq(nil)
      expect(persona.temperature).to eq(nil)
    end

    it "logs staff action when updating a persona" do
      persona = Fabricate(:ai_persona, name: "original_name", description: "original description")

      # Update the persona
      put "/admin/plugins/discourse-ai/ai-personas/#{persona.id}.json",
          params: {
            ai_persona: {
              name: "updated_name",
              description: "updated description",
            },
          }

      expect(response).to have_http_status(:ok)
      persona.reload
      expect(persona.name).to eq("updated_name")
      expect(persona.description).to eq("updated description")

      # Now verify the log was created with the right subject
      history =
        UserHistory.where(
          action: UserHistory.actions[:custom_staff],
          custom_type: "update_ai_persona",
        ).last
      expect(history).to be_present
      expect(history.subject).to eq("updated_name") # Verify subject is set to the new name
    end

    it "supports updating rag params" do
      persona = Fabricate(:ai_persona, name: "test_bot2")

      put "/admin/plugins/discourse-ai/ai-personas/#{persona.id}.json",
          params: {
            ai_persona: {
              rag_chunk_tokens: "102",
              rag_chunk_overlap_tokens: "12",
              rag_conversation_chunks: "13",
              rag_llm_model_id: llm_model.id,
              question_consolidator_llm_id: llm_model.id,
            },
          }

      expect(response).to have_http_status(:ok)
      persona.reload

      expect(persona.rag_chunk_tokens).to eq(102)
      expect(persona.rag_chunk_overlap_tokens).to eq(12)
      expect(persona.rag_conversation_chunks).to eq(13)
      expect(persona.rag_llm_model_id).to eq(llm_model.id)
      expect(persona.question_consolidator_llm_id).to eq(llm_model.id)
    end

    it "supports updating vision params" do
      persona = Fabricate(:ai_persona, name: "test_bot2")
      put "/admin/plugins/discourse-ai/ai-personas/#{persona.id}.json",
          params: {
            ai_persona: {
              vision_enabled: true,
              vision_max_pixels: 512 * 512,
            },
          }

      expect(response).to have_http_status(:ok)
      persona.reload

      expect(persona.vision_enabled).to eq(true)
      expect(persona.vision_max_pixels).to eq(512 * 512)
    end

    it "does not allow temperature and top p changes on stock personas" do
      put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
          params: {
            ai_persona: {
              top_p: 0.5,
              temperature: 0.1,
            },
          }

      expect(response).to have_http_status(:unprocessable_entity)
    end

    context "with valid params" do
      it "updates the requested ai_persona" do
        put "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}.json",
            params: {
              ai_persona: {
                name: "SuperBot",
                enabled: false,
                tools: ["search"],
              },
            }

        expect(response).to have_http_status(:ok)
        expect(response.content_type).to include("application/json")

        ai_persona.reload
        expect(ai_persona.name).to eq("SuperBot")
        expect(ai_persona.enabled).to eq(false)
        expect(ai_persona.tools).to eq([["search", nil, false]])
      end
    end

    context "with system personas" do
      it "does not allow editing of system prompts" do
        put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
            params: {
              ai_persona: {
                system_prompt: "you are not a helpful bot",
              },
            }

        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.parsed_body["errors"].join).not_to be_blank
        expect(response.parsed_body["errors"].join).not_to include("en.discourse")
      end

      it "does not allow editing of tools" do
        put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
            params: {
              ai_persona: {
                tools: %w[SearchCommand ImageCommand],
              },
            }

        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.parsed_body["errors"].join).not_to be_blank
        expect(response.parsed_body["errors"].join).not_to include("en.discourse")
      end

      it "does not allow editing of name and description cause it is localized" do
        put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
            params: {
              ai_persona: {
                name: "bob",
                description: "the bob",
              },
            }

        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.parsed_body["errors"].join).not_to be_blank
        expect(response.parsed_body["errors"].join).not_to include("en.discourse")
      end

      it "does allow some actions" do
        put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
            params: {
              ai_persona: {
                allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_1]],
                enabled: false,
                priority: 989,
              },
            }

        expect(response).to be_successful
      end
    end

    context "with invalid params" do
      it "renders a JSON response with errors for the ai_persona" do
        put "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}.json",
            params: {
              ai_persona: {
                name: "",
              },
            } # invalid attribute
        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.content_type).to include("application/json")
      end
    end
  end

  describe "GET #export" do
    fab!(:ai_tool) do
      AiTool.create!(
        name: "Test Tool",
        tool_name: "test_tool",
        description: "A test tool",
        script: "function invoke(params) { return 'test'; }",
        parameters: [{ name: "query", type: "string", required: true }],
        summary: "Test tool summary",
        created_by_id: admin.id,
      )
    end

    fab!(:persona_with_tools) do
      AiPersona.create!(
        name: "ToolMaster",
        description: "A persona with custom tools",
        system_prompt: "You are a tool master",
        tools: [
          ["SearchCommand", { "base_query" => "test" }, true],
          ["custom-#{ai_tool.id}", { "max_results" => 10 }, false],
        ],
        temperature: 0.8,
        top_p: 0.9,
        response_format: [{ type: "string", key: "summary" }],
        examples: [["user example", "assistant example"]],
        default_llm_id: llm_model.id,
      )
    end

    it "exports a persona as JSON" do
      get "/admin/plugins/discourse-ai/ai-personas/#{persona_with_tools.id}/export.json"

      expect(response).to be_successful
      expect(response.headers["Content-Disposition"]).to include("attachment")
      expect(response.headers["Content-Disposition"]).to include("toolmaster.json")

      json = response.parsed_body
      expect(json["meta"]["version"]).to eq("1.0")
      expect(json["meta"]["exported_at"]).to be_present

      persona_data = json["persona"]
      expect(persona_data["name"]).to eq("ToolMaster")
      expect(persona_data["description"]).to eq("A persona with custom tools")
      expect(persona_data["system_prompt"]).to eq("You are a tool master")
      expect(persona_data["temperature"]).to eq(0.8)
      expect(persona_data["top_p"]).to eq(0.9)
      expect(persona_data["response_format"]).to eq([{ "type" => "string", "key" => "summary" }])
      expect(persona_data["examples"]).to eq([["user example", "assistant example"]])

      # Check that custom tool ID is replaced with tool_name
      expect(persona_data["tools"]).to include(
        ["SearchCommand", { "base_query" => "test" }, true],
        ["custom-test_tool", { "max_results" => 10 }, false],
      )

      # Check custom tools are exported
      expect(json["custom_tools"]).to be_an(Array)
      expect(json["custom_tools"].length).to eq(1)

      custom_tool = json["custom_tools"].first
      expect(custom_tool["identifier"]).to eq("test_tool")
      expect(custom_tool["name"]).to eq("Test Tool")
      expect(custom_tool["description"]).to eq("A test tool")
      expect(custom_tool["script"]).to eq("function invoke(params) { return 'test'; }")
      expect(custom_tool["parameters"]).to eq(
        [{ "name" => "query", "type" => "string", "required" => true }],
      )
    end

    it "handles personas without custom tools" do
      persona = Fabricate(:ai_persona, tools: ["SearchCommand"])

      get "/admin/plugins/discourse-ai/ai-personas/#{persona.id}/export.json"

      expect(response).to be_successful
      json = response.parsed_body
      expect(json["custom_tools"]).to eq([])
      expect(json["persona"]["tools"]).to eq(["SearchCommand"])
    end

    it "returns 404 for non-existent persona" do
      get "/admin/plugins/discourse-ai/ai-personas/999999/export.json"
      expect(response).to have_http_status(:not_found)
    end
  end

  describe "POST #import" do
    let(:valid_import_data) do
      {
        meta: {
          version: "1.0",
          exported_at: Time.zone.now.iso8601,
        },
        persona: {
          name: "ImportedPersona",
          description: "An imported persona",
          system_prompt: "You are an imported assistant",
          temperature: 0.7,
          top_p: 0.8,
          response_format: [{ type: "string", key: "answer" }],
          examples: [["hello", "hi there"]],
          tools: ["SearchCommand", ["ReadCommand", { max_length: 1000 }, true]],
        },
        custom_tools: [],
      }
    end

    it "imports a new persona successfully" do
      expect {
        post "/admin/plugins/discourse-ai/ai-personas/import.json",
             params: valid_import_data,
             as: :json
        expect(response).to have_http_status(:created)
      }.to change(AiPersona, :count).by(1)

      persona = AiPersona.find_by(name: "ImportedPersona")
      expect(persona).to be_present
      expect(persona.description).to eq("An imported persona")
      expect(persona.system_prompt).to eq("You are an imported assistant")
      expect(persona.temperature).to eq(0.7)
      expect(persona.top_p).to eq(0.8)
      expect(persona.response_format).to eq([{ "type" => "string", "key" => "answer" }])
      expect(persona.examples).to eq([["hello", "hi there"]])
      expect(persona.tools).to eq(
        ["SearchCommand", ["ReadCommand", { "max_length" => 1000 }, true]],
      )
    end

    it "imports a persona with custom tools" do
      import_data_with_tools = valid_import_data.deep_dup
      import_data_with_tools[:persona][:tools] = [
        "SearchCommand",
        ["custom-my_custom_tool", { param1: "value1" }, false],
      ]
      import_data_with_tools[:custom_tools] = [
        {
          identifier: "my_custom_tool",
          name: "My Custom Tool",
          description: "A custom tool for testing",
          tool_name: "my_custom_tool",
          parameters: [{ name: "param1", type: "string", required: true }],
          summary: "Custom tool summary",
          script: "function invoke(params) { return params.param1; }",
        },
      ]

      expect {
        post "/admin/plugins/discourse-ai/ai-personas/import.json",
             params: import_data_with_tools,
             as: :json
      }.to change(AiPersona, :count).by(1).and change(AiTool, :count).by(1)

      expect(response).to have_http_status(:created)

      persona = AiPersona.find_by(name: "ImportedPersona")
      expect(persona).to be_present

      tool = AiTool.find_by(tool_name: "my_custom_tool")
      expect(tool).to be_present
      expect(tool.name).to eq("My Custom Tool")
      expect(tool.description).to eq("A custom tool for testing")
      expect(tool.script).to eq("function invoke(params) { return params.param1; }")

      # Check that the tool reference uses the ID
      expect(persona.tools).to include(
        "SearchCommand",
        ["custom-#{tool.id}", { "param1" => "value1" }, false],
      )
    end

    it "prevents importing duplicate personas by default" do
      _existing_persona = Fabricate(:ai_persona, name: "ImportedPersona")

      post "/admin/plugins/discourse-ai/ai-personas/import.json",
           params: valid_import_data,
           as: :json

      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.parsed_body["errors"].join).to include("ImportedPersona")
    end

    it "allows overwriting existing personas with force=true" do
      existing_persona =
        Fabricate(:ai_persona, name: "ImportedPersona", description: "Old description")

      import_data = valid_import_data.merge(force: true)

      expect {
        post "/admin/plugins/discourse-ai/ai-personas/import.json", params: import_data, as: :json
      }.not_to change(AiPersona, :count)

      expect(response).to have_http_status(:ok)

      existing_persona.reload
      expect(existing_persona.description).to eq("An imported persona")
      expect(existing_persona.system_prompt).to eq("You are an imported assistant")
    end

    it "handles invalid import data gracefully" do
      invalid_data = { invalid: "data" }

      post "/admin/plugins/discourse-ai/ai-personas/import.json", params: invalid_data, as: :json

      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.parsed_body["errors"]).to be_present
    end

    it "handles missing persona data" do
      invalid_data = { meta: { version: "1.0" } }

      post "/admin/plugins/discourse-ai/ai-personas/import.json", params: invalid_data, as: :json

      expect(response).to have_http_status(:unprocessable_entity)
    end

    it "logs staff action when importing a new persona" do
      post "/admin/plugins/discourse-ai/ai-personas/import.json",
           params: valid_import_data,
           as: :json

      expect(response).to have_http_status(:created)

      history =
        UserHistory.where(
          action: UserHistory.actions[:custom_staff],
          custom_type: "create_ai_persona",
        ).last
      expect(history).to be_present
      expect(history.subject).to eq("ImportedPersona")
    end

    it "logs staff action when updating an existing persona" do
      _existing_persona = Fabricate(:ai_persona, name: "ImportedPersona")

      import_data = valid_import_data.merge(force: true)

      post "/admin/plugins/discourse-ai/ai-personas/import.json", params: import_data, as: :json

      expect(response).to have_http_status(:ok)

      history =
        UserHistory.where(
          action: UserHistory.actions[:custom_staff],
          custom_type: "update_ai_persona",
        ).last
      expect(history).to be_present
      expect(history.subject).to eq("ImportedPersona")
    end
  end

  describe "DELETE #destroy" do
    it "destroys the requested ai_persona" do
      expect {
        delete "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}.json"

        expect(response).to have_http_status(:no_content)
      }.to change(AiPersona, :count).by(-1)
    end

    it "logs staff action when deleting a persona" do
      # Capture persona details before deletion
      _persona_id = ai_persona.id
      persona_name = ai_persona.name

      # Delete the persona
      delete "/admin/plugins/discourse-ai/ai-personas/#{ai_persona.id}.json"
      expect(response).to have_http_status(:no_content)

      # Now verify the log was created with the right subject
      history =
        UserHistory.where(
          action: UserHistory.actions[:custom_staff],
          custom_type: "delete_ai_persona",
        ).last
      expect(history).to be_present
      expect(history.subject).to eq(persona_name) # Verify subject is set to name
    end

    it "is not allowed to delete system personas" do
      expect {
        delete "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json"
        expect(response).to have_http_status(:unprocessable_entity)
        expect(response.parsed_body["errors"].join).not_to be_blank
        # let's make sure this is translated
        expect(response.parsed_body["errors"].join).not_to include("en.discourse")
      }.not_to change(AiPersona, :count)
    end
  end

  describe "#stream_reply" do
    fab!(:llm) { Fabricate(:llm_model, name: "fake_llm", provider: "fake") }
    let(:fake_endpoint) { DiscourseAi::Completions::Endpoints::Fake }

    before { fake_endpoint.delays = [] }

    after { fake_endpoint.reset! }

    it "ensures persona exists" do
      post "/admin/plugins/discourse-ai/ai-personas/stream-reply.json"
      expect(response).to have_http_status(:unprocessable_entity)
      # this ensures localization key is actually in the yaml
      expect(response.body).to include("persona_name")
    end

    it "ensures question exists" do
      ai_persona.update!(default_llm_id: llm.id)

      post "/admin/plugins/discourse-ai/ai-personas/stream-reply.json",
           params: {
             persona_id: ai_persona.id,
             user_unique_id: "site:test.com:user_id:1",
           }
      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.body).to include("query")
    end

    it "ensure persona has a user specified" do
      ai_persona.update!(default_llm_id: llm.id)

      post "/admin/plugins/discourse-ai/ai-personas/stream-reply.json",
           params: {
             persona_id: ai_persona.id,
             query: "how are you today?",
             user_unique_id: "site:test.com:user_id:1",
           }

      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.body).to include("associated")
    end

    def validate_streamed_response(raw_http, expected)
      lines = raw_http.split("\r\n")

      header_lines, _, payload_lines = lines.chunk { |l| l == "" }.map(&:last)

      preamble = (<<~PREAMBLE).strip
        HTTP/1.1 200 OK
        Content-Type: text/plain; charset=utf-8
        Transfer-Encoding: chunked
        Cache-Control: no-cache, no-store, must-revalidate
        Connection: close
        X-Accel-Buffering: no
        X-Content-Type-Options: nosniff
      PREAMBLE

      expect(header_lines.join("\n")).to eq(preamble)

      parsed = +""

      context_info = nil

      payload_lines.each_slice(2) do |size, data|
        size = size.to_i(16)
        data = data.to_s
        expect(data.bytesize).to eq(size)

        if size > 0
          json = JSON.parse(data)
          parsed << json["partial"].to_s

          context_info = json if json["topic_id"]
        end
      end

      expect(parsed).to eq(expected)

      context_info
    end

    it "is able to create a new conversation" do
      Jobs.run_immediately!
      # trust level 0
      SiteSetting.ai_bot_allowed_groups = "10"

      fake_endpoint.fake_content = ["This is a test! Testing!", "An amazing title"]

      ai_persona.create_user!
      ai_persona.update!(
        allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
        default_llm_id: llm.id,
        allow_personal_messages: true,
        system_prompt: "you are a helpful bot",
      )

      io_out, io_in = IO.pipe

      post "/admin/plugins/discourse-ai/ai-personas/stream-reply.json",
           params: {
             persona_name: ai_persona.name,
             query: "how are you today?",
             user_unique_id: "site:test.com:user_id:1",
             preferred_username: "test_user",
             custom_instructions: "To be appended to system prompt",
           },
           env: {
             "rack.hijack" => lambda { io_in },
           }

      # this is a fake response but it catches errors
      expect(response).to have_http_status(:no_content)

      raw = io_out.read
      context_info = validate_streamed_response(raw, "This is a test! Testing!")

      system_prompt = fake_endpoint.previous_calls[-2][:dialect].prompt.messages.first[:content]

      expect(system_prompt).to eq("you are a helpful bot\nTo be appended to system prompt")

      expect(context_info["topic_id"]).to be_present
      topic = Topic.find(context_info["topic_id"])
      last_post = topic.posts.order(:created_at).last
      expect(last_post.raw).to eq("This is a test! Testing!")

      user_post = topic.posts.find_by(post_number: 1)
      expect(user_post.raw).to eq("how are you today?")

      # need ai persona and user
      expect(topic.topic_allowed_users.count).to eq(2)
      expect(topic.archetype).to eq(Archetype.private_message)
      expect(topic.title).to eq("An amazing title")
      expect(topic.posts.count).to eq(2)

      tool_call =
        DiscourseAi::Completions::ToolCall.new(name: "categories", parameters: {}, id: "tool_1")

      fake_endpoint.fake_content = [tool_call, "this is the response after the tool"]
      # this simplifies function calls
      fake_endpoint.chunk_count = 1

      ai_persona.update!(tools: ["Categories"])

      # lets also unstage the user and add the user to tl0
      # this will ensure there are no feedback loops
      new_user = user_post.user
      new_user.update!(staged: false)
      Group.user_trust_level_change!(new_user.id, new_user.trust_level)

      # double check this happened and user is in group
      personas = AiPersona.allowed_modalities(user: new_user.reload, allow_personal_messages: true)
      expect(personas.count).to eq(1)

      io_out, io_in = IO.pipe

      post "/admin/plugins/discourse-ai/ai-personas/stream-reply.json",
           params: {
             persona_id: ai_persona.id,
             query: "how are you now?",
             user_unique_id: "site:test.com:user_id:1",
             preferred_username: "test_user",
             topic_id: context_info["topic_id"],
           },
           env: {
             "rack.hijack" => lambda { io_in },
           }

      # this is a fake response but it catches errors
      expect(response).to have_http_status(:no_content)

      raw = io_out.read
      context_info = validate_streamed_response(raw, "this is the response after the tool")

      topic = topic.reload
      last_post = topic.posts.order(:created_at).last

      expect(last_post.raw).to end_with("this is the response after the tool")
      # function call is visible in the post
      expect(last_post.raw[0..8]).to eq("<details>")

      user_post = topic.posts.find_by(post_number: 3)
      expect(user_post.raw).to eq("how are you now?")
      expect(user_post.user.username).to eq("test_user")
      expect(user_post.user.custom_fields).to eq(
        { "ai-stream-conversation-unique-id" => "site:test.com:user_id:1" },
      )

      expect(topic.posts.count).to eq(4)
    end
  end
end
