defmodule ToweropsWeb.DeviceLive.FormTest do
  use ToweropsWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  alias Towerops.Snmp.Device

  setup :register_and_log_in_user

  setup %{user: user} do
    {:ok, organization} =
      Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

    {:ok, site} =
      Towerops.Sites.create_site(%{
        name: "Test Site",
        organization_id: organization.id
      })

    %{organization: organization, site: site}
  end

  describe "edit page rendering" do
    test "renders edit page when device has no SNMP discovery data (bug fix for nil boolean)", %{
      conn: conn,
      site: site,
      organization: organization
    } do
      # Regression test for production bug where visiting edit page for a device
      # with no SNMP discovery data would crash with "expected boolean, got nil"
      # because mikrotik_device?/1 could return nil instead of false.

      # Create a device without any snmp_device association (before SNMP discovery)
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Test Device",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_port: 161,
          monitoring_enabled: true,
          check_interval_seconds: 300
        })

      # Ensure device has no snmp_device (this is the bug condition)
      device_with_snmp = Towerops.Repo.preload(device, :snmp_device)
      assert is_nil(device_with_snmp.snmp_device)

      # Visit edit page - should not crash with "expected boolean, got nil" error
      assert {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")

      # Page should render successfully
      assert html =~ "Edit Device"
      assert html =~ device.name
    end

    test "renders MikroTik section when device is MikroTik", %{
      conn: conn,
      site: site,
      organization: organization
    } do
      # Create a device with MikroTik SNMP discovery data
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "MikroTik Router",
          ip_address: "192.168.1.254",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_port: 161,
          monitoring_enabled: true,
          check_interval_seconds: 300
        })

      # Create SNMP device with MikroTik manufacturer (using Towerops.Snmp.Device schema)
      _snmp_device =
        %Device{}
        |> Device.changeset(%{
          device_id: device.id,
          manufacturer: "MikroTik",
          sys_descr: "RouterOS 7.8",
          sys_object_id: "1.3.6.1.4.1.14988.1",
          sys_uptime: 0,
          sys_name: "test-router"
        })
        |> Towerops.Repo.insert!()

      # Visit edit page
      {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")

      # Page should render successfully
      assert html =~ "Edit Device"
      assert html =~ device.name

      # MikroTik section SHOULD be visible for all users with MikroTik devices
      assert html =~ "MikroTik API Configuration"
    end

    test "does not render MikroTik section when device is not MikroTik", %{
      conn: conn,
      site: site,
      organization: organization
    } do
      alias Device, as: SnmpDevice

      # Create a device with non-MikroTik SNMP discovery data
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Cisco Switch",
          ip_address: "192.168.1.253",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_version: "2c",
          snmp_port: 161,
          monitoring_enabled: true,
          check_interval_seconds: 300
        })

      # Create SNMP device with Cisco manufacturer
      _snmp_device =
        %SnmpDevice{}
        |> SnmpDevice.changeset(%{
          device_id: device.id,
          manufacturer: "Cisco Systems",
          sys_descr: "Cisco IOS",
          sys_object_id: "1.3.6.1.4.1.9",
          sys_uptime: 0,
          sys_name: "test-switch"
        })
        |> Towerops.Repo.insert!()

      # Visit edit page
      {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")

      # Page should render successfully
      assert html =~ "Edit Device"
      assert html =~ device.name

      # MikroTik section SHOULD NOT be visible for non-MikroTik devices
      refute html =~ "MikroTik API Configuration"
    end
  end

  describe "new device form" do
    test "renders new device page", %{conn: conn} do
      {:ok, _view, html} = live(conn, ~p"/devices/new")

      assert html =~ "New Device"
    end

    test "pre-fills from query params", %{conn: conn} do
      {:ok, _view, html} = live(conn, ~p"/devices/new?name=Prefilled&ip_address=10.0.0.1")

      assert html =~ "Prefilled"
      assert html =~ "10.0.0.1"
    end

    test "validates device form on change", %{conn: conn} do
      {:ok, view, _html} = live(conn, ~p"/devices/new")

      html =
        view
        |> form("#device-form", device: %{name: "", ip_address: ""})
        |> render_change()

      assert html =~ "New Device"
    end

    test "submits new device form", %{conn: conn} do
      {:ok, view, _html} = live(conn, ~p"/devices/new")

      result =
        view
        |> form("#device-form",
          device: %{
            name: "New Test Device",
            ip_address: "10.0.0.100"
          }
        )
        |> render_submit()

      case result do
        {:error, {:live_redirect, %{to: to}}} ->
          assert to =~ "tab=overview"

        html when is_binary(html) ->
          # Form re-rendered (possibly validation), page still alive
          assert html =~ "New Device" or html =~ "New Test Device"
      end
    end

    test "switches monitoring mode", %{conn: conn} do
      {:ok, view, _html} = live(conn, ~p"/devices/new")

      html = render_click(view, "switch_monitoring_mode", %{"mode" => "icmp_only"})
      assert html =~ "New Device"
    end
  end

  describe "edit device form" do
    setup %{site: site, organization: organization} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Edit Me",
          ip_address: "192.168.1.50",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true
        })

      %{device: device}
    end

    test "renders edit page with current values", %{conn: conn, device: device} do
      {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")

      assert html =~ "Edit Device"
      assert html =~ "Edit Me"
    end

    test "validates edit form on change", %{conn: conn, device: device} do
      {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")

      html =
        view
        |> form("#device-form", device: %{name: "Updated Name"})
        |> render_change()

      assert html =~ "Updated Name"
    end

    test "saves edited device", %{conn: conn, device: device} do
      {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")

      assert {:error, {:live_redirect, %{to: to}}} =
               view
               |> form("#device-form", device: %{name: "Updated Router"})
               |> render_submit()

      assert to =~ device.id
    end

    test "deletes device", %{conn: conn, device: device} do
      {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")

      result = render_click(view, "delete")

      case result do
        {:error, {:live_redirect, %{to: "/devices" <> _}}} ->
          assert true

        html when is_binary(html) ->
          # If it redirects via push_navigate, the view dies
          refute Process.alive?(view.pid)
      end
    end

    test "trigger_discovery with SNMP disabled shows error", %{conn: conn, site: site, organization: organization} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "No SNMP",
          ip_address: "192.168.1.99",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: false
        })

      {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")

      html = render_click(view, "trigger_discovery")
      assert html =~ "SNMP is not enabled"
    end
  end

  describe "device role selector" do
    setup %{site: site, organization: organization} do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Role Test Device",
          ip_address: "192.168.1.60",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true
        })

      %{device: device}
    end

    test "new device form shows device role dropdown", %{conn: conn} do
      {:ok, _view, html} = live(conn, ~p"/devices/new")

      assert html =~ "Device Role"
      assert html =~ "Auto-detect"
      assert html =~ "Router"
      assert html =~ "Switch"
      assert html =~ "Firewall"
    end

    test "edit device form shows device role dropdown", %{conn: conn, device: device} do
      {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")

      assert html =~ "Device Role"
      assert html =~ "Auto-detect"
    end

    test "selecting a role sets device_role_source to manual on save", %{conn: conn, device: device} do
      {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")

      assert {:error, {:live_redirect, %{to: to}}} =
               view
               |> form("#device-form", device: %{device_role: "router"})
               |> render_submit()

      assert to =~ device.id

      updated_device = Towerops.Devices.get_device!(device.id)
      assert updated_device.device_role == "router"
      assert updated_device.device_role_source == "manual"
    end

    test "selecting auto-detect clears device_role and sets source to inferred", %{
      conn: conn,
      site: site,
      organization: organization
    } do
      # Create device with a manually set role
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Manual Role Device",
          ip_address: "192.168.1.61",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          device_role: "switch",
          device_role_source: "manual"
        })

      {:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")

      # Select empty string (auto-detect)
      assert {:error, {:live_redirect, %{to: to}}} =
               view
               |> form("#device-form", device: %{device_role: ""})
               |> render_submit()

      assert to =~ device.id

      updated_device = Towerops.Devices.get_device!(device.id)
      assert is_nil(updated_device.device_role)
      assert updated_device.device_role_source == "inferred"
    end

    test "edit form shows current role as selected", %{
      conn: conn,
      site: site,
      organization: organization
    } do
      {:ok, device} =
        Towerops.Devices.create_device(%{
          name: "Firewall Device",
          ip_address: "192.168.1.62",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          device_role: "firewall",
          device_role_source: "manual"
        })

      {:ok, _view, html} = live(conn, ~p"/devices/#{device.id}/edit")

      # The select should show the current role selected
      assert html =~ "device_role"
      assert html =~ "firewall"
    end
  end

  describe "non_routable_ip?/1" do
    # Access private function via Module.get_attribute or test via public interface
    # Since these are private functions, we test through the validation behavior

    test "RFC1918 10.0.0.0/8 range" do
      assert non_routable?("10.0.0.1")
      assert non_routable?("10.255.255.255")
      assert non_routable?("10.100.50.25")
    end

    test "RFC1918 172.16.0.0/12 range" do
      assert non_routable?("172.16.0.1")
      assert non_routable?("172.31.255.255")
      assert non_routable?("172.20.10.5")
      refute non_routable?("172.15.0.1")
      refute non_routable?("172.32.0.1")
    end

    test "RFC1918 192.168.0.0/16 range" do
      assert non_routable?("192.168.0.1")
      assert non_routable?("192.168.255.255")
      assert non_routable?("192.168.1.100")
      refute non_routable?("192.167.1.1")
      refute non_routable?("192.169.1.1")
    end

    test "RFC6598 CGNAT 100.64.0.0/10 range" do
      assert non_routable?("100.64.0.1")
      assert non_routable?("100.127.255.255")
      assert non_routable?("100.100.50.25")
      refute non_routable?("100.63.255.255")
      refute non_routable?("100.128.0.1")
    end

    test "public IPs are not flagged" do
      refute non_routable?("8.8.8.8")
      refute non_routable?("1.1.1.1")
      refute non_routable?("203.0.113.1")
      refute non_routable?("198.51.100.1")
    end

    test "invalid IPs are not flagged" do
      refute non_routable?("invalid")
      refute non_routable?("999.999.999.999")
      refute non_routable?("")
    end

    test "IPv6 addresses are not flagged" do
      refute non_routable?("::1")
      refute non_routable?("fe80::1")
      refute non_routable?("2001:db8::1")
    end
  end

  # Helper to test the private function logic
  # Duplicated here for testing since the actual function is private
  defp non_routable?(ip_string) do
    case ip_string |> String.to_charlist() |> :inet.parse_address() do
      {:ok, {a, b, _c, _d}} -> non_routable_ipv4_range?(a, b)
      _ -> false
    end
  end

  defp non_routable_ipv4_range?(10, _b), do: true
  defp non_routable_ipv4_range?(172, b) when b >= 16 and b <= 31, do: true
  defp non_routable_ipv4_range?(192, 168), do: true
  defp non_routable_ipv4_range?(100, b) when b >= 64 and b <= 127, do: true
  defp non_routable_ipv4_range?(_a, _b), do: false
end
