defmodule Towerops.EquipmentTest do
  use Towerops.DataCase

  alias Towerops.Devices
  alias Towerops.Devices.Device

  describe "device" do
    import Towerops.AccountsFixtures

    alias Device, as: DeviceSchema

    setup do
      user = user_fixture()
      {: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, user: user}
    end

    @valid_attrs %{
      name: "Router 1",
      ip_address: "192.168.1.1",
      description: "Main router",
      monitoring_enabled: true,
      check_interval_seconds: 300
    }
    @update_attrs %{
      name: "Updated Router",
      ip_address: "192.168.1.2",
      description: "Updated",
      monitoring_enabled: false
    }
    @invalid_attrs %{name: nil, ip_address: nil}

    test "list_site_devices/1 returns all devices for a site", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
      assert Devices.list_site_devices(site.id) == [device]
    end

    test "list_organization_devices/1 returns all devices for an organization", %{
      organization: organization,
      site: site
    } do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
      result = Devices.list_organization_devices(organization.id)
      assert length(result) == 1
      assert hd(result).id == device.id
    end

    test "list_monitored_devices/0 returns only device with monitoring enabled", %{
      site: site
    } do
      {:ok, monitored} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))

      {:ok, _not_monitored} =
        Devices.create_device(%{
          name: "Not Monitored",
          ip_address: "192.168.1.3",
          site_id: site.id,
          monitoring_enabled: false
        })

      result = Devices.list_monitored_devices()
      assert length(result) == 1
      assert hd(result).id == monitored.id
    end

    test "get_device!/1 returns the device with given id", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
      assert Devices.get_device!(device.id).id == device.id
    end

    test "get_site_equipment!/2 returns device for specific site", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))

      assert Devices.get_site_device!(site.id, device.id).id == device.id
    end

    test "create_device/1 with valid data creates device", %{site: site} do
      attrs = Map.put(@valid_attrs, :site_id, site.id)
      assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
      assert device.name == "Router 1"
      assert device.ip_address == "192.168.1.1"
      assert device.status == :unknown
      assert device.monitoring_enabled == true
      assert device.check_interval_seconds == 300
    end

    test "create_device/1 with valid IPv6 address", %{site: site} do
      attrs = Map.put(@valid_attrs, :ip_address, "2001:0db8:85a3::8a2e:0370:7334")
      attrs = Map.put(attrs, :site_id, site.id)
      assert {:ok, %DeviceSchema{}} = Devices.create_device(attrs)
    end

    test "create_device/1 with invalid IP address returns error", %{site: site} do
      attrs = Map.put(@valid_attrs, :ip_address, "invalid-ip")
      attrs = Map.put(attrs, :site_id, site.id)
      assert {:error, changeset} = Devices.create_device(attrs)
      assert "must be a valid IPv4 or IPv6 address" in errors_on(changeset).ip_address
    end

    test "create_device/1 with invalid data returns error changeset" do
      assert {:error, %Ecto.Changeset{}} = Devices.create_device(@invalid_attrs)
    end

    test "update_device/2 with valid data updates the device", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))

      assert {:ok, %DeviceSchema{} = device} =
               Devices.update_device(device, @update_attrs)

      assert device.name == "Updated Router"
      assert device.ip_address == "192.168.1.2"
      assert device.monitoring_enabled == false
    end

    test "update_device/2 with invalid data returns error changeset", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))

      assert {:error, %Ecto.Changeset{}} =
               Devices.update_device(device, @invalid_attrs)

      assert Devices.get_device!(device.id).name == device.name
    end

    test "delete_device/1 deletes the device", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
      assert {:ok, %DeviceSchema{}} = Devices.delete_device(device)
      assert_raise Ecto.NoResultsError, fn -> Devices.get_device!(device.id) end
    end

    test "change_device/1 returns an device changeset", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
      assert %Ecto.Changeset{} = Devices.change_device(device)
    end

    test "update_device_status/2 updates status and timestamps", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))

      assert {:ok, updated} = Devices.update_device_status(device, :up)
      assert updated.status == :up
      assert updated.last_checked_at
      assert updated.last_status_change_at
    end

    test "update_device_status/2 only updates last_checked_at if status unchanged", %{
      site: site
    } do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
      {:ok, device} = Devices.update_device_status(device, :up)
      first_change_at = device.last_status_change_at
      first_checked_at = device.last_checked_at

      # Sleep for 1+ second to ensure timestamps differ (truncated to seconds in update_device_status)
      Process.sleep(1010)
      {:ok, updated} = Devices.update_device_status(device, :up)
      # Status change timestamp should not change
      assert updated.last_status_change_at == first_change_at
      # But checked_at should be updated
      assert DateTime.after?(updated.last_checked_at, first_checked_at)
    end

    test "list_snmp_enabled_devices/0 returns only device with SNMP enabled", %{site: site} do
      {:ok, snmp_enabled} =
        Devices.create_device(%{
          name: "SNMP Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: true,
          snmp_community: "public"
        })

      {:ok, _snmp_disabled} =
        Devices.create_device(%{
          name: "Non-SNMP Router",
          ip_address: "192.168.1.2",
          site_id: site.id,
          snmp_enabled: false
        })

      result = Devices.list_snmp_enabled_devices()
      assert length(result) == 1
      assert hd(result).id == snmp_enabled.id
    end

    test "update_snmp_poll_time/1 updates last_snmp_poll_at timestamp", %{site: site} do
      {:ok, device} = Devices.create_device(Map.put(@valid_attrs, :site_id, site.id))
      assert device.last_snmp_poll_at == nil

      assert {:ok, updated} = Devices.update_snmp_poll_time(device)
      assert updated.last_snmp_poll_at
      assert DateTime.before?(updated.last_snmp_poll_at, DateTime.utc_now())
    end

    test "create_device/1 with SNMP enabled and nil community string succeeds", %{site: site} do
      attrs = %{
        name: "SNMP Router",
        ip_address: "192.168.1.1",
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: nil
      }

      assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
      assert device.snmp_enabled == true
      assert device.snmp_community == nil
    end

    test "create_device/1 with SNMP enabled and empty community string succeeds", %{site: site} do
      attrs = %{
        name: "SNMP Router",
        ip_address: "192.168.1.1",
        site_id: site.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: ""
      }

      # Empty string should be converted to nil by the database
      assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
      assert device.snmp_enabled == true
      # Empty strings are stored as nil in the database
      assert device.snmp_community == nil || device.snmp_community == ""
    end
  end

  describe "SNMP configuration inheritance" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {: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, user: user}
    end

    test "get_snmp_config/1 returns device-level community string when set", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: true,
          snmp_community: "device-community"
        })

      config = Devices.get_snmp_config(device)
      assert config.community == "device-community"
      assert config.source == :device
    end

    test "get_snmp_config/1 returns site-level community when device has nil", %{
      organization: _organization,
      site: site
    } do
      # Update site with SNMP community
      {:ok, site} =
        Towerops.Sites.update_site(site, %{
          snmp_community: "site-community",
          snmp_version: "2c"
        })

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: true,
          snmp_community: nil
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      assert config.community == "site-community"
      assert config.source == :site
    end

    test "get_snmp_config/1 returns org-level community when device and site have nil", %{
      organization: organization,
      site: site
    } do
      # Update organization with SNMP community
      {:ok, _organization} =
        Towerops.Organizations.update_organization(organization, %{
          snmp_community: "org-community",
          snmp_version: "2c"
        })

      # Reload site with updated organization
      site = Towerops.Repo.preload(site, :organization, force: true)

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: true,
          snmp_community: nil
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      assert config.community == "org-community"
      assert config.source == :organization
    end

    test "get_snmp_config/1 returns nil when all levels have nil", %{site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: true,
          snmp_community: nil
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      assert config.community == nil
      assert config.source == :default
    end

    test "get_snmp_config/1 prioritizes device over site over org", %{
      organization: organization,
      site: site
    } do
      # Set community at all levels
      {:ok, _organization} =
        Towerops.Organizations.update_organization(organization, %{
          snmp_community: "org-community",
          snmp_version: "2c"
        })

      {:ok, site} =
        Towerops.Sites.update_site(site, %{
          snmp_community: "site-community",
          snmp_version: "2c"
        })

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          snmp_enabled: true,
          snmp_community: "device-community"
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      # Should use device-level community
      assert config.community == "device-community"
      assert config.source == :device
    end
  end

  describe "events" do
    import Towerops.AccountsFixtures

    alias Towerops.Devices.Event

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

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

      {:ok, device} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id
        })

      %{device: device}
    end

    test "create_event/1 creates an device event", %{device: device} do
      attrs = %{
        device_id: device.id,
        event_type: "interface_up",
        severity: "info",
        message: "Interface came online",
        metadata: %{"interface" => "eth0"},
        occurred_at: DateTime.truncate(DateTime.utc_now(), :second)
      }

      assert {:ok, %Event{} = event} = Devices.create_event(attrs)
      assert event.device_id == device.id
      assert event.event_type == "interface_up"
      assert event.severity == "info"
      assert event.message == "Interface came online"
      assert event.metadata == %{"interface" => "eth0"}
    end

    test "create_event/1 with invalid data returns error changeset" do
      assert {:error, %Ecto.Changeset{}} = Devices.create_event(%{})
    end

    test "list_devices_events/2 returns events for device ordered by most recent", %{
      device: device
    } do
      now = DateTime.utc_now()

      {:ok, event1} =
        Devices.create_event(%{
          device_id: device.id,
          event_type: "interface_up",
          severity: "info",
          message: "Interface came up",
          occurred_at: DateTime.add(now, -3600, :second)
        })

      {:ok, event2} =
        Devices.create_event(%{
          device_id: device.id,
          event_type: "interface_down",
          severity: "warning",
          message: "Interface went down",
          occurred_at: DateTime.add(now, -1800, :second)
        })

      events = Devices.list_devices_events(device.id)
      assert length(events) == 2
      # Should be ordered by most recent first
      assert hd(events).id == event2.id
      assert List.last(events).id == event1.id
    end

    test "list_devices_events/2 limits results", %{device: device} do
      now = DateTime.utc_now()

      # Create 10 events
      for i <- 1..10 do
        Devices.create_event(%{
          device_id: device.id,
          event_type: "interface_down",
          severity: "warning",
          message: "Interface down event #{i}",
          occurred_at: DateTime.add(now, -i * 60, :second)
        })
      end

      events = Devices.list_devices_events(device.id, 5)
      assert length(events) == 5
    end
  end
end
