defmodule Towerops.Admin.AuditLoggerTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures

  alias Towerops.Admin.AuditLog
  alias Towerops.Admin.AuditLogger

  setup do
    actor = user_fixture()
    target = user_fixture()

    %{actor: actor, target: target}
  end

  describe "log_event/3" do
    test "creates an audit log with a conn", %{actor: actor, target: target} do
      conn =
        :get
        |> Plug.Test.conn("/admin/users")
        |> Plug.Conn.put_req_header("user-agent", "TestBrowser/1.0")
        |> Plug.Conn.put_req_header("x-forwarded-for", "203.0.113.50")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_viewed", conn,
                 actor_id: actor.id,
                 target_user_id: target.id,
                 metadata: %{reason: "support ticket"}
               )

      assert log.action == "user_data_viewed"
      assert log.superuser_id == actor.id
      assert log.target_user_id == target.id
      assert log.ip_address.address == "203.0.113.50"
      assert log.user_agent == "TestBrowser/1.0"
      assert log.request_path == "/admin/users"
      assert log.metadata.reason == "support ticket"
    end

    test "creates an audit log without a conn (nil)", %{actor: actor} do
      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", nil,
                 actor_id: actor.id,
                 target_user_id: actor.id,
                 metadata: %{export_format: "json"}
               )

      assert log.action == "user_data_exported"
      assert log.superuser_id == actor.id
      assert log.ip_address == nil
      assert log.user_agent == nil
      assert log.request_path == nil
    end

    test "creates an audit log with no opts", %{actor: _actor} do
      conn = Plug.Test.conn(:get, "/test")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", conn)

      assert log.action == "user_data_exported"
      assert log.superuser_id == nil
      assert log.target_user_id == nil
      assert log.request_path == "/test"
    end

    test "returns error for invalid action" do
      assert {:error, changeset} = AuditLogger.log_event("bogus_action")
      assert %{action: ["is invalid"]} = errors_on(changeset)
    end

    test "stores data_accessed when provided", %{actor: actor, target: target} do
      data = %{fields: ["email", "phone"]}

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_viewed", nil,
                 actor_id: actor.id,
                 target_user_id: target.id,
                 data_accessed: data
               )

      assert log.data_accessed.fields == ["email", "phone"]
    end
  end

  describe "log_user_data_viewed/4" do
    test "logs user data viewed event with conn metadata", %{actor: actor, target: target} do
      conn =
        :get
        |> Plug.Test.conn("/admin/users/#{target.id}")
        |> Plug.Conn.put_req_header("user-agent", "Mozilla/5.0")
        |> Plug.Conn.put_req_header("x-forwarded-for", "10.0.0.1")

      data_accessed = %{fields: ["email", "name"]}

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_user_data_viewed(conn, actor.id, target.id, data_accessed)

      assert log.action == "user_data_viewed"
      assert log.superuser_id == actor.id
      assert log.target_user_id == target.id
      assert log.data_accessed.fields == ["email", "name"]
      assert log.ip_address.address == "10.0.0.1"
      assert log.user_agent == "Mozilla/5.0"
    end
  end

  describe "log_user_data_exported/2" do
    test "logs user data export event", %{actor: actor} do
      conn = Plug.Test.conn(:post, "/settings/export")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_user_data_exported(conn, actor.id)

      assert log.action == "user_data_exported"
      assert log.superuser_id == actor.id
      assert log.target_user_id == actor.id
      assert log.metadata.export_format == "json"
    end
  end

  describe "log_user_profile_updated/3" do
    test "logs profile update with changed fields", %{actor: actor} do
      conn = Plug.Test.conn(:put, "/settings/profile")
      fields_changed = ["email", "name"]

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_user_profile_updated(conn, actor.id, fields_changed)

      assert log.action == "user_profile_updated"
      assert log.superuser_id == actor.id
      assert log.target_user_id == actor.id
      assert log.data_accessed.fields_changed == ["email", "name"]
    end
  end

  describe "log_device_created/4" do
    test "logs device creation with metadata", %{actor: actor} do
      conn = Plug.Test.conn(:post, "/devices")
      device_id = Ecto.UUID.generate()

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_device_created(conn, actor.id, device_id, "Core Router")

      assert log.action == "device_created"
      assert log.superuser_id == actor.id
      assert log.metadata.device_id == device_id
      assert log.metadata.device_name == "Core Router"
    end
  end

  describe "log_device_updated/4" do
    test "logs device update with changed fields", %{actor: actor} do
      conn = Plug.Test.conn(:put, "/devices/1")
      device_id = Ecto.UUID.generate()

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_device_updated(conn, actor.id, device_id, ["hostname", "ip"])

      assert log.action == "device_updated"
      assert log.superuser_id == actor.id
      assert log.metadata.device_id == device_id
      assert log.data_accessed.fields_changed == ["hostname", "ip"]
    end
  end

  describe "log_device_deleted/4" do
    test "logs device deletion with metadata", %{actor: actor} do
      conn = Plug.Test.conn(:delete, "/devices/1")
      device_id = Ecto.UUID.generate()

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_device_deleted(conn, actor.id, device_id, "Old Switch")

      assert log.action == "device_deleted"
      assert log.superuser_id == actor.id
      assert log.metadata.device_id == device_id
      assert log.metadata.device_name == "Old Switch"
    end
  end

  describe "log_org_data_accessed/4" do
    test "logs organization data access", %{actor: actor} do
      conn = Plug.Test.conn(:get, "/orgs/settings")
      org_id = Ecto.UUID.generate()

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_org_data_accessed(conn, actor.id, org_id, "viewed_members")

      assert log.action == "org_data_accessed"
      assert log.superuser_id == actor.id
      assert log.metadata.organization_id == org_id
      assert log.metadata.action == "viewed_members"
    end
  end

  describe "log_monitoring_data_queried/4" do
    test "logs monitoring query event", %{actor: actor} do
      conn = Plug.Test.conn(:get, "/monitoring")
      device_id = Ecto.UUID.generate()

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_monitoring_data_queried(conn, actor.id, device_id, "24h")

      assert log.action == "monitoring_data_queried"
      assert log.superuser_id == actor.id
      assert log.metadata.device_id == device_id
      assert log.metadata.time_range == "24h"
    end
  end

  describe "log_failed_access_attempt/4" do
    test "logs failed access with resource and reason", %{actor: actor} do
      conn =
        :get
        |> Plug.Test.conn("/admin/secret")
        |> Plug.Conn.put_req_header("x-forwarded-for", "192.168.1.100")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_failed_access_attempt(
                 conn,
                 actor.id,
                 "/admin/secret",
                 "insufficient_permissions"
               )

      assert log.action == "failed_access_attempt"
      assert log.superuser_id == actor.id
      assert log.metadata.resource == "/admin/secret"
      assert log.metadata.reason == "insufficient_permissions"
      assert log.ip_address.address == "192.168.1.100"
    end
  end

  describe "log_privilege_escalation/5" do
    test "logs role change with context", %{actor: actor} do
      conn = Plug.Test.conn(:post, "/admin/roles")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_privilege_escalation(
                 conn,
                 actor.id,
                 "member",
                 "admin",
                 "org_promotion"
               )

      assert log.action == "privilege_escalation"
      assert log.superuser_id == actor.id
      assert log.metadata.from_role == "member"
      assert log.metadata.to_role == "admin"
      assert log.metadata.context == "org_promotion"
    end
  end

  describe "IP address extraction" do
    test "extracts IP from x-forwarded-for header", %{actor: actor} do
      conn =
        :get
        |> Plug.Test.conn("/test")
        |> Plug.Conn.put_req_header("x-forwarded-for", "198.51.100.42")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", conn,
                 actor_id: actor.id,
                 target_user_id: actor.id
               )

      assert log.ip_address.address == "198.51.100.42"
    end

    test "extracts first IP from comma-separated x-forwarded-for", %{actor: actor} do
      conn =
        :get
        |> Plug.Test.conn("/test")
        |> Plug.Conn.put_req_header("x-forwarded-for", "198.51.100.42, 10.0.0.1, 172.16.0.1")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", conn,
                 actor_id: actor.id,
                 target_user_id: actor.id
               )

      assert log.ip_address.address == "198.51.100.42"
    end

    test "falls back to remote_ip when x-forwarded-for is absent", %{actor: actor} do
      conn = %{Plug.Test.conn(:get, "/test") | remote_ip: {10, 20, 30, 40}}

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", conn,
                 actor_id: actor.id,
                 target_user_id: actor.id
               )

      assert log.ip_address.address == "10.20.30.40"
    end
  end

  describe "user agent extraction" do
    test "extracts user-agent header from conn", %{actor: actor} do
      conn =
        :get
        |> Plug.Test.conn("/test")
        |> Plug.Conn.put_req_header("user-agent", "CustomAgent/2.0")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", conn,
                 actor_id: actor.id,
                 target_user_id: actor.id
               )

      assert log.user_agent == "CustomAgent/2.0"
    end

    test "returns nil user_agent when header is absent", %{actor: actor} do
      conn = Plug.Test.conn(:get, "/test")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", conn,
                 actor_id: actor.id,
                 target_user_id: actor.id
               )

      assert log.user_agent == nil
    end
  end

  describe "request path extraction" do
    test "extracts request_path from conn", %{actor: actor} do
      conn = Plug.Test.conn(:get, "/admin/dashboard")

      assert {:ok, %AuditLog{} = log} =
               AuditLogger.log_event("user_data_exported", conn,
                 actor_id: actor.id,
                 target_user_id: actor.id
               )

      assert log.request_path == "/admin/dashboard"
    end
  end
end
